release 2.0 beta 2 (#3757)

* do not traverse into invoice template subdirectories (#3735)
* fix security open api definition
* fix currency can be null, removed fluent interface
* merged release 1.30.3
* allow to pre-fill timesheet metafields via URL
* fix api description
* added test accounts with simpler names and password
* upgrade to Symfony 6.2
* removed FrameworkExtraBundle (by Sensio) and replaced with new native SF annotations
* fixed symfony 6.2 deprecations
* fixed #3768
This commit is contained in:
Kevin Papst
2023-01-18 14:47:48 +01:00
committed by GitHub
parent 6e0500972e
commit 0e91dd886e
141 changed files with 1600 additions and 1415 deletions

View File

@@ -26,7 +26,7 @@
"ext-zip": "*", "ext-zip": "*",
"composer/package-versions-deprecated": "^1.8", "composer/package-versions-deprecated": "^1.8",
"composer/semver": "^3.3", "composer/semver": "^3.3",
"doctrine/doctrine-bundle": "^2.0", "doctrine/doctrine-bundle": "^2.7",
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.8", "doctrine/orm": "^2.8",
"endroid/qr-code": "^4.4.9", "endroid/qr-code": "^4.4.9",
@@ -49,7 +49,6 @@
"scheb/2fa-backup-code": "^6.2", "scheb/2fa-backup-code": "^6.2",
"scheb/2fa-bundle": "^6.2", "scheb/2fa-bundle": "^6.2",
"scheb/2fa-totp": "^6.2", "scheb/2fa-totp": "^6.2",
"sensio/framework-extra-bundle": "^6.0",
"symfony/asset": "^6.0", "symfony/asset": "^6.0",
"symfony/console": "^6.0", "symfony/console": "^6.0",
"symfony/dotenv": "^6.0", "symfony/dotenv": "^6.0",
@@ -68,7 +67,7 @@
"symfony/translation": "^6.0", "symfony/translation": "^6.0",
"symfony/twig-bundle": "^6.0", "symfony/twig-bundle": "^6.0",
"symfony/validator": "^6.0", "symfony/validator": "^6.0",
"symfony/webpack-encore-bundle": "^1.5", "symfony/webpack-encore-bundle": "^1.16",
"symfony/yaml": "^6.0", "symfony/yaml": "^6.0",
"twig/cssinliner-extra": "^3.0", "twig/cssinliner-extra": "^3.0",
"twig/extra-bundle": "^3.0", "twig/extra-bundle": "^3.0",
@@ -196,7 +195,7 @@
"symfony": { "symfony": {
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR", "id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
"allow-contrib": true, "allow-contrib": true,
"require": "6.0.*" "require": "6.2.*"
} }
} }
} }

1279
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ return [
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],

View File

@@ -4,7 +4,7 @@ fos_rest:
cache_dir: '%kernel.cache_dir%/fos_rest' cache_dir: '%kernel.cache_dir%/fos_rest'
routing_loader: false routing_loader: false
body_converter: body_converter:
enabled: true enabled: false
serializer: serializer:
serialize_null: true serialize_null: true
view: view:

View File

@@ -63,4 +63,4 @@ nelmio_api_doc:
in: header in: header
security: security:
- X-AUTH-USER: [] - X-AUTH-USER: []
- X-AUTH-TOKEN: [] X-AUTH-TOKEN: []

View File

@@ -1,3 +0,0 @@
sensio_framework_extra:
router:
annotations: false

View File

@@ -2075,16 +2075,6 @@ parameters:
count: 1 count: 1
path: src/DataFixtures/TeamFixtures.php path: src/DataFixtures/TeamFixtures.php
-
message: "#^Cannot access offset 1 on mixed\\.$#"
count: 1
path: src/DataFixtures/TimesheetFixtures.php
-
message: "#^Cannot access offset 2 on mixed\\.$#"
count: 1
path: src/DataFixtures/TimesheetFixtures.php
- -
message: "#^Cannot call method setTimestamp\\(\\) on DateTime\\|null\\.$#" message: "#^Cannot call method setTimestamp\\(\\) on DateTime\\|null\\.$#"
count: 1 count: 1
@@ -2095,51 +2085,21 @@ parameters:
count: 1 count: 1
path: src/DataFixtures/TimesheetFixtures.php path: src/DataFixtures/TimesheetFixtures.php
-
message: "#^Method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:createTimesheetEntry\\(\\) has no return type specified\\.$#"
count: 1
path: src/DataFixtures/TimesheetFixtures.php
-
message: "#^Method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:findRandom\\(\\) has no return type specified\\.$#"
count: 1
path: src/DataFixtures/TimesheetFixtures.php
- -
message: "#^Parameter \\#1 \\$begin of method App\\\\Entity\\\\Timesheet\\:\\:setBegin\\(\\) expects DateTime, DateTime\\|null given\\.$#" message: "#^Parameter \\#1 \\$begin of method App\\\\Entity\\\\Timesheet\\:\\:setBegin\\(\\) expects DateTime, DateTime\\|null given\\.$#"
count: 1 count: 1
path: src/DataFixtures/TimesheetFixtures.php path: src/DataFixtures/TimesheetFixtures.php
-
message: "#^Parameter \\#1 \\$className of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getRepository\\(\\) expects class\\-string\\<object\\>, string given\\.$#"
count: 2
path: src/DataFixtures/TimesheetFixtures.php
- -
message: "#^Parameter \\#1 \\$description of method App\\\\Entity\\\\Timesheet\\:\\:setDescription\\(\\) expects string, string\\|null given\\.$#" message: "#^Parameter \\#1 \\$description of method App\\\\Entity\\\\Timesheet\\:\\:setDescription\\(\\) expects string, string\\|null given\\.$#"
count: 1 count: 1
path: src/DataFixtures/TimesheetFixtures.php path: src/DataFixtures/TimesheetFixtures.php
-
message: "#^Parameter \\#1 \\$min of function rand expects int, mixed given\\.$#"
count: 1
path: src/DataFixtures/TimesheetFixtures.php
- -
message: "#^Parameter \\#1 \\$user of method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:createTimesheetEntry\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#" message: "#^Parameter \\#1 \\$user of method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:createTimesheetEntry\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 2 count: 2
path: src/DataFixtures/TimesheetFixtures.php path: src/DataFixtures/TimesheetFixtures.php
-
message: "#^Parameter \\#2 \\$max of function rand expects int, mixed given\\.$#"
count: 1
path: src/DataFixtures/TimesheetFixtures.php
-
message: "#^Unable to resolve the template type T in call to method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getRepository\\(\\)$#"
count: 2
path: src/DataFixtures/TimesheetFixtures.php
- -
message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:getUserDefinition\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:getUserDefinition\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -2150,16 +2110,6 @@ parameters:
count: 1 count: 1
path: src/DataFixtures/UserFixtures.php path: src/DataFixtures/UserFixtures.php
-
message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:loadDefaultAccounts\\(\\) has no return type specified\\.$#"
count: 1
path: src/DataFixtures/UserFixtures.php
-
message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:loadTestUsers\\(\\) has no return type specified\\.$#"
count: 1
path: src/DataFixtures/UserFixtures.php
- -
message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:createPermissionParameter\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:createPermissionParameter\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
count: 1 count: 1

View File

@@ -21,14 +21,14 @@ use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
#[Route(path: '/actions')] #[Route(path: '/actions')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Actions')] #[OA\Tag(name: 'Actions')]
final class ActionsController extends BaseApiController final class ActionsController extends BaseApiController
{ {

View File

@@ -26,15 +26,15 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/activities')] #[Route(path: '/activities')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Activity')] #[OA\Tag(name: 'Activity')]
final class ActivityController extends BaseApiController final class ActivityController extends BaseApiController
{ {
@@ -179,7 +179,7 @@ final class ActivityController extends BaseApiController
/** /**
* Update an existing activity * Update an existing activity
*/ */
#[Security("is_granted('edit', activity)")] #[IsGranted('edit', 'activity')]
#[OA\Patch(description: 'Update an existing activity, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated activity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))])] #[OA\Patch(description: 'Update an existing activity, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated activity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityEditForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityEditForm'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Activity ID to update', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Activity ID to update', required: true)]
@@ -217,7 +217,7 @@ final class ActivityController extends BaseApiController
/** /**
* Sets the value of a meta-field for an existing activity * Sets the value of a meta-field for an existing activity
*/ */
#[Security("is_granted('edit', activity)")] #[IsGranted('edit', 'activity')]
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))] #[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Activity record ID to set the meta-field value for', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Activity record ID to set the meta-field value for', required: true)]
#[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])]
@@ -250,7 +250,7 @@ final class ActivityController extends BaseApiController
/** /**
* Returns a collection of all rates for one activity * Returns a collection of all rates for one activity
*/ */
#[Security("is_granted('edit', activity)")] #[IsGranted('edit', 'activity')]
#[OA\Response(response: 200, description: 'Returns a collection of activity rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ActivityRate')))] #[OA\Response(response: 200, description: 'Returns a collection of activity rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ActivityRate')))]
#[OA\Parameter(name: 'id', in: 'path', description: 'The activity whose rates will be returned', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The activity whose rates will be returned', required: true)]
#[Rest\Get(path: '/{id}/rates', name: 'get_activity_rates', requirements: ['id' => '\d+'])] #[Rest\Get(path: '/{id}/rates', name: 'get_activity_rates', requirements: ['id' => '\d+'])]
@@ -269,15 +269,14 @@ final class ActivityController extends BaseApiController
/** /**
* Deletes one rate for an activity * Deletes one rate for an activity
*/ */
#[Security("is_granted('edit', activity)")] #[IsGranted('edit', 'activity')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The activity whose rate will be removed', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The activity whose rate will be removed', required: true)]
#[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)] #[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_activity_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])] #[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_activity_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])]
#[Entity('rate', expr: 'repository.find(rateId)')] public function deleteRateAction(Activity $activity, #[MapEntity(mapping: ['rateId' => 'id'])] ActivityRate $rate): Response
public function deleteRateAction(Activity $activity, ActivityRate $rate): Response
{ {
if ($rate->getActivity() !== $activity) { if ($rate->getActivity() !== $activity) {
throw $this->createNotFoundException(); throw $this->createNotFoundException();
@@ -293,7 +292,7 @@ final class ActivityController extends BaseApiController
/** /**
* Adds a new rate to an activity * Adds a new rate to an activity
*/ */
#[Security("is_granted('edit', activity)")] #[IsGranted('edit', 'activity')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ActivityRate'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ActivityRate'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The activity to add the rate for', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The activity to add the rate for', required: true)]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityRateForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityRateForm'))]

View File

@@ -17,10 +17,10 @@ use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Default')] #[OA\Tag(name: 'Default')]
final class ConfigurationController extends BaseApiController final class ConfigurationController extends BaseApiController
{ {

View File

@@ -26,15 +26,15 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/customers')] #[Route(path: '/customers')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Customer')] #[OA\Tag(name: 'Customer')]
final class CustomerController extends BaseApiController final class CustomerController extends BaseApiController
{ {
@@ -156,7 +156,7 @@ final class CustomerController extends BaseApiController
/** /**
* Update an existing customer * Update an existing customer
*/ */
#[Security("is_granted('edit', customer)")] #[IsGranted('edit', 'customer')]
#[OA\Patch(description: 'Update an existing customer, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated customer', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))])] #[OA\Patch(description: 'Update an existing customer, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated customer', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerEditForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerEditForm'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Customer ID to update', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Customer ID to update', required: true)]
@@ -194,7 +194,7 @@ final class CustomerController extends BaseApiController
/** /**
* Sets the value of a meta-field for an existing customer * Sets the value of a meta-field for an existing customer
*/ */
#[Security("is_granted('edit', customer)")] #[IsGranted('edit', 'customer')]
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))] #[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Customer record ID to set the meta-field value for', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Customer record ID to set the meta-field value for', required: true)]
#[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])]
@@ -227,7 +227,7 @@ final class CustomerController extends BaseApiController
/** /**
* Returns a collection of all rates for one customer * Returns a collection of all rates for one customer
*/ */
#[Security("is_granted('edit', customer)")] #[IsGranted('edit', 'customer')]
#[OA\Response(response: 200, description: 'Returns a collection of customer rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/CustomerRate')))] #[OA\Response(response: 200, description: 'Returns a collection of customer rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/CustomerRate')))]
#[OA\Parameter(name: 'id', in: 'path', description: 'The customer whose rates will be returned', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The customer whose rates will be returned', required: true)]
#[Rest\Get(path: '/{id}/rates', name: 'get_customer_rates', requirements: ['id' => '\d+'])] #[Rest\Get(path: '/{id}/rates', name: 'get_customer_rates', requirements: ['id' => '\d+'])]
@@ -246,15 +246,14 @@ final class CustomerController extends BaseApiController
/** /**
* Deletes one rate for a customer * Deletes one rate for a customer
*/ */
#[Security("is_granted('edit', customer)")] #[IsGranted('edit', 'customer')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The customer whose rate will be removed', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The customer whose rate will be removed', required: true)]
#[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)] #[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_customer_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])] #[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_customer_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])]
#[Entity('rate', expr: 'repository.find(rateId)')] public function deleteRateAction(Customer $customer, #[MapEntity(mapping: ['rateId' => 'id'])] CustomerRate $rate): Response
public function deleteRateAction(Customer $customer, CustomerRate $rate): Response
{ {
if ($rate->getCustomer() !== $customer) { if ($rate->getCustomer() !== $customer) {
throw $this->createNotFoundException(); throw $this->createNotFoundException();
@@ -270,7 +269,7 @@ final class CustomerController extends BaseApiController
/** /**
* Adds a new rate to a customer * Adds a new rate to a customer
*/ */
#[Security("is_granted('edit', customer)")] #[IsGranted('edit', 'customer')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/CustomerRate'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/CustomerRate'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The customer to add the rate for', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The customer to add the rate for', required: true)]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerRateForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerRateForm'))]

View File

@@ -27,16 +27,16 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraints;
#[Route(path: '/projects')] #[Route(path: '/projects')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Project')] #[OA\Tag(name: 'Project')]
final class ProjectController extends BaseApiController final class ProjectController extends BaseApiController
{ {
@@ -209,7 +209,7 @@ final class ProjectController extends BaseApiController
/** /**
* Update an existing project * Update an existing project
*/ */
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
#[OA\Patch(description: 'Update an existing project, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated project', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))])] #[OA\Patch(description: 'Update an existing project, you can pass all or just a subset of all attributes', responses: [new OA\Response(response: 200, description: 'Returns the updated project', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectEditForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectEditForm'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Project ID to update', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Project ID to update', required: true)]
@@ -249,7 +249,7 @@ final class ProjectController extends BaseApiController
/** /**
* Sets the value of a meta-field for an existing project * Sets the value of a meta-field for an existing project
*/ */
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))] #[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Project record ID to set the meta-field value for', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Project record ID to set the meta-field value for', required: true)]
#[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])]
@@ -282,7 +282,7 @@ final class ProjectController extends BaseApiController
/** /**
* Returns a collection of all rates for one project * Returns a collection of all rates for one project
*/ */
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
#[OA\Response(response: 200, description: 'Returns a collection of project rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ProjectRate')))] #[OA\Response(response: 200, description: 'Returns a collection of project rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ProjectRate')))]
#[OA\Parameter(name: 'id', in: 'path', description: 'The project whose rates will be returned', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The project whose rates will be returned', required: true)]
#[Rest\Get(path: '/{id}/rates', name: 'get_project_rates', requirements: ['id' => '\d+'])] #[Rest\Get(path: '/{id}/rates', name: 'get_project_rates', requirements: ['id' => '\d+'])]
@@ -301,15 +301,14 @@ final class ProjectController extends BaseApiController
/** /**
* Deletes one rate for a project * Deletes one rate for a project
*/ */
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The project whose rate will be removed', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The project whose rate will be removed', required: true)]
#[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)] #[OA\Parameter(name: 'rateId', in: 'path', description: 'The rate to remove', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_project_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])] #[Rest\Delete(path: '/{id}/rates/{rateId}', name: 'delete_project_rate', requirements: ['id' => '\d+', 'rateId' => '\d+'])]
#[Entity('rate', expr: 'repository.find(rateId)')] public function deleteRateAction(Project $project, #[MapEntity(mapping: ['rateId' => 'id'])] ProjectRate $rate): Response
public function deleteRateAction(Project $project, ProjectRate $rate): Response
{ {
if ($rate->getProject() !== $project) { if ($rate->getProject() !== $project) {
throw $this->createNotFoundException(); throw $this->createNotFoundException();
@@ -325,7 +324,7 @@ final class ProjectController extends BaseApiController
/** /**
* Adds a new rate to a project * Adds a new rate to a project
*/ */
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ProjectRate'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ProjectRate'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The project to add the rate for', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The project to add the rate for', required: true)]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectRateForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectRateForm'))]

View File

@@ -16,8 +16,8 @@ use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface; use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor; use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;

View File

@@ -18,10 +18,10 @@ use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Default')] #[OA\Tag(name: 'Default')]
final class StatusController extends BaseApiController final class StatusController extends BaseApiController
{ {

View File

@@ -18,13 +18,13 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/tags')] #[Route(path: '/tags')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Tag')] #[OA\Tag(name: 'Tag')]
final class TagController extends BaseApiController final class TagController extends BaseApiController
{ {
@@ -94,7 +94,7 @@ final class TagController extends BaseApiController
/** /**
* Delete a tag * Delete a tag
*/ */
#[Security("is_granted('delete_tag')")] #[IsGranted('delete_tag')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'HTTP code 204 for a successful delete')])] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'HTTP code 204 for a successful delete')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Tag ID to delete', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Tag ID to delete', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]

View File

@@ -21,15 +21,15 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/teams')] #[Route(path: '/teams')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Team')] #[OA\Tag(name: 'Team')]
final class TeamController extends BaseApiController final class TeamController extends BaseApiController
{ {
@@ -44,7 +44,7 @@ final class TeamController extends BaseApiController
/** /**
* Fetch all existing teams (which are visible to the user) * Fetch all existing teams (which are visible to the user)
*/ */
#[Security("is_granted('view_team')")] #[IsGranted('view_team')]
#[OA\Response(response: 200, description: 'Returns the collection of teams', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamCollection')))] #[OA\Response(response: 200, description: 'Returns the collection of teams', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamCollection')))]
#[Rest\Get(path: '', name: 'get_teams')] #[Rest\Get(path: '', name: 'get_teams')]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
@@ -62,7 +62,7 @@ final class TeamController extends BaseApiController
/** /**
* Returns one team * Returns one team
*/ */
#[Security("is_granted('view_team')")] #[IsGranted('view_team')]
#[OA\Response(response: 200, description: 'Returns one team entity', content: new OA\JsonContent(ref: '#/components/schemas/Team'))] #[OA\Response(response: 200, description: 'Returns one team entity', content: new OA\JsonContent(ref: '#/components/schemas/Team'))]
#[Rest\Get(path: '/{id}', name: 'get_team', requirements: ['id' => '\d+'])] #[Rest\Get(path: '/{id}', name: 'get_team', requirements: ['id' => '\d+'])]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
@@ -78,7 +78,7 @@ final class TeamController extends BaseApiController
/** /**
* Delete a team * Delete a team
*/ */
#[Security("is_granted('delete_team')")] #[IsGranted('delete_team')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one team')])] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one team')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Team ID to delete', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Team ID to delete', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
@@ -96,7 +96,7 @@ final class TeamController extends BaseApiController
/** /**
* Creates a new team * Creates a new team
*/ */
#[Security("is_granted('create_team')")] #[IsGranted('create_team')]
#[OA\Post(description: 'Creates a new team and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Post(description: 'Creates a new team and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))]
#[Rest\Post(path: '', name: 'post_team')] #[Rest\Post(path: '', name: 'post_team')]
@@ -127,7 +127,7 @@ final class TeamController extends BaseApiController
/** /**
* Update an existing team * Update an existing team
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Patch(description: 'Update an existing team, you can pass all or just a subset of all attributes (passing members will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Patch(description: 'Update an existing team, you can pass all or just a subset of all attributes (passing members will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Team ID to update', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Team ID to update', required: true)]
@@ -167,15 +167,14 @@ final class TeamController extends BaseApiController
/** /**
* Add a new member to a team * Add a new member to a team
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new user to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new user to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The team which will receive the new member', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The team which will receive the new member', required: true)]
#[OA\Parameter(name: 'userId', in: 'path', description: 'The team member to add (User ID)', required: true)] #[OA\Parameter(name: 'userId', in: 'path', description: 'The team member to add (User ID)', required: true)]
#[Rest\Post(path: '/{id}/members/{userId}', name: 'post_team_member', requirements: ['id' => '\d+', 'userId' => '\d+'])] #[Rest\Post(path: '/{id}/members/{userId}', name: 'post_team_member', requirements: ['id' => '\d+', 'userId' => '\d+'])]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Entity('member', expr: 'repository.find(userId)')] public function postMemberAction(Team $team, #[MapEntity(mapping: ['userId' => 'id'])] User $member): Response
public function postMemberAction(Team $team, User $member): Response
{ {
if ($member->isInTeam($team)) { if ($member->isInTeam($team)) {
throw new BadRequestHttpException('User is already member of the team'); throw new BadRequestHttpException('User is already member of the team');
@@ -194,15 +193,14 @@ final class TeamController extends BaseApiController
/** /**
* Removes a member from the team * Removes a member from the team
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a user from the team. The teamlead cannot be removed.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a user from the team. The teamlead cannot be removed.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The team from which the member will be removed', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The team from which the member will be removed', required: true)]
#[OA\Parameter(name: 'userId', in: 'path', description: 'The team member to remove (User ID)', required: true)] #[OA\Parameter(name: 'userId', in: 'path', description: 'The team member to remove (User ID)', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Rest\Delete(path: '/{id}/members/{userId}', name: 'delete_team_member', requirements: ['id' => '\d+', 'userId' => '\d+'])] #[Rest\Delete(path: '/{id}/members/{userId}', name: 'delete_team_member', requirements: ['id' => '\d+', 'userId' => '\d+'])]
#[Entity('member', expr: 'repository.find(userId)')] public function deleteMemberAction(Team $team, #[MapEntity(mapping: ['userId' => 'id'])] User $member): Response
public function deleteMemberAction(Team $team, User $member): Response
{ {
if (!$member->isInTeam($team)) { if (!$member->isInTeam($team)) {
throw new BadRequestHttpException('User is not a member of the team'); throw new BadRequestHttpException('User is not a member of the team');
@@ -225,15 +223,14 @@ final class TeamController extends BaseApiController
/** /**
* Grant the team access to a customer * Grant the team access to a customer
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new customer to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new customer to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)]
#[OA\Parameter(name: 'customerId', in: 'path', description: 'The customer to grant acecess to (Customer ID)', required: true)] #[OA\Parameter(name: 'customerId', in: 'path', description: 'The customer to grant acecess to (Customer ID)', required: true)]
#[Rest\Post(path: '/{id}/customers/{customerId}', name: 'post_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])] #[Rest\Post(path: '/{id}/customers/{customerId}', name: 'post_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Entity('customer', expr: 'repository.find(customerId)')] public function postCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer): Response
public function postCustomerAction(Team $team, Customer $customer): Response
{ {
if ($team->hasCustomer($customer)) { if ($team->hasCustomer($customer)) {
throw new BadRequestHttpException('Team has already access to customer'); throw new BadRequestHttpException('Team has already access to customer');
@@ -252,15 +249,14 @@ final class TeamController extends BaseApiController
/** /**
* Revokes access for a customer from a team * Revokes access for a customer from a team
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a customer from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a customer from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)]
#[OA\Parameter(name: 'customerId', in: 'path', description: 'The customer to remove (Customer ID)', required: true)] #[OA\Parameter(name: 'customerId', in: 'path', description: 'The customer to remove (Customer ID)', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Rest\Delete(path: '/{id}/customers/{customerId}', name: 'delete_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])] #[Rest\Delete(path: '/{id}/customers/{customerId}', name: 'delete_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])]
#[Entity('customer', expr: 'repository.find(customerId)')] public function deleteCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer): Response
public function deleteCustomerAction(Team $team, Customer $customer): Response
{ {
if (!$team->hasCustomer($customer)) { if (!$team->hasCustomer($customer)) {
throw new BadRequestHttpException('Customer is not assigned to the team'); throw new BadRequestHttpException('Customer is not assigned to the team');
@@ -279,15 +275,14 @@ final class TeamController extends BaseApiController
/** /**
* Grant the team access to a project * Grant the team access to a project
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new project to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new project to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)]
#[OA\Parameter(name: 'projectId', in: 'path', description: 'The project to grant acecess to (Project ID)', required: true)] #[OA\Parameter(name: 'projectId', in: 'path', description: 'The project to grant acecess to (Project ID)', required: true)]
#[Rest\Post(path: '/{id}/projects/{projectId}', name: 'post_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])] #[Rest\Post(path: '/{id}/projects/{projectId}', name: 'post_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Entity('project', expr: 'repository.find(projectId)')] public function postProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project): Response
public function postProjectAction(Team $team, Project $project): Response
{ {
if ($team->hasProject($project)) { if ($team->hasProject($project)) {
throw new BadRequestHttpException('Team has already access to project'); throw new BadRequestHttpException('Team has already access to project');
@@ -306,15 +301,14 @@ final class TeamController extends BaseApiController
/** /**
* Revokes access for a project from a team * Revokes access for a project from a team
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a project from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a project from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)]
#[OA\Parameter(name: 'projectId', in: 'path', description: 'The project to remove (Project ID)', required: true)] #[OA\Parameter(name: 'projectId', in: 'path', description: 'The project to remove (Project ID)', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Rest\Delete(path: '/{id}/projects/{projectId}', name: 'delete_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])] #[Rest\Delete(path: '/{id}/projects/{projectId}', name: 'delete_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])]
#[Entity('project', expr: 'repository.find(projectId)')] public function deleteProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project): Response
public function deleteProjectAction(Team $team, Project $project): Response
{ {
if (!$team->hasProject($project)) { if (!$team->hasProject($project)) {
throw new BadRequestHttpException('Project is not assigned to the team'); throw new BadRequestHttpException('Project is not assigned to the team');
@@ -333,15 +327,14 @@ final class TeamController extends BaseApiController
/** /**
* Grant the team access to an activity * Grant the team access to an activity
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new activity to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new activity to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The team that is granted access', required: true)]
#[OA\Parameter(name: 'activityId', in: 'path', description: 'The activity to grant acecess to (Activity ID)', required: true)] #[OA\Parameter(name: 'activityId', in: 'path', description: 'The activity to grant acecess to (Activity ID)', required: true)]
#[Rest\Post(path: '/{id}/activities/{activityId}', name: 'post_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])] #[Rest\Post(path: '/{id}/activities/{activityId}', name: 'post_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Entity('activity', expr: 'repository.find(activityId)')] public function postActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity): Response
public function postActivityAction(Team $team, Activity $activity): Response
{ {
if ($team->hasActivity($activity)) { if ($team->hasActivity($activity)) {
throw new BadRequestHttpException('Team has already access to activity'); throw new BadRequestHttpException('Team has already access to activity');
@@ -360,15 +353,14 @@ final class TeamController extends BaseApiController
/** /**
* Revokes access for an activity from a team * Revokes access for an activity from a team
*/ */
#[Security("is_granted('edit_team')")] #[IsGranted('edit_team')]
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a activity from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])] #[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a activity from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'The team whose permission will be revoked', required: true)]
#[OA\Parameter(name: 'activityId', in: 'path', description: 'The activity to remove (Activity ID)', required: true)] #[OA\Parameter(name: 'activityId', in: 'path', description: 'The activity to remove (Activity ID)', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')] #[ApiSecurity(name: 'apiToken')]
#[Rest\Delete(path: '/{id}/activities/{activityId}', name: 'delete_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])] #[Rest\Delete(path: '/{id}/activities/{activityId}', name: 'delete_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])]
#[Entity('activity', expr: 'repository.find(activityId)')] public function deleteActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity): Response
public function deleteActivityAction(Team $team, Activity $activity): Response
{ {
if (!$team->hasActivity($activity)) { if (!$team->hasActivity($activity)) {
throw new BadRequestHttpException('Activity is not assigned to the team'); throw new BadRequestHttpException('Activity is not assigned to the team');

View File

@@ -33,16 +33,17 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraints;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[Route(path: '/timesheets')] #[Route(path: '/timesheets')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Timesheet')] #[OA\Tag(name: 'Timesheet')]
final class TimesheetController extends BaseApiController final class TimesheetController extends BaseApiController
{ {
@@ -69,7 +70,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Returns a collection of timesheet records (which are visible to the user) * Returns a collection of timesheet records (which are visible to the user)
*/ */
#[Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')")] #[IsGranted(new Expression("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')"))]
#[OA\Response(response: 200, description: 'Returns a collection of timesheet records. The datetime fields are given in the users local time including the timezone offset (ISO-8601).', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollection')))] #[OA\Response(response: 200, description: 'Returns a collection of timesheet records. The datetime fields are given in the users local time including the timezone offset (ISO-8601).', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollection')))]
#[Rest\Get(path: '', name: 'get_timesheets')] #[Rest\Get(path: '', name: 'get_timesheets')]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
@@ -256,7 +257,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Returns one timesheet record * Returns one timesheet record
*/ */
#[Security("is_granted('view', timesheet)")] #[IsGranted('view', 'timesheet')]
#[OA\Response(response: 200, description: 'Returns one timesheet record. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] #[OA\Response(response: 200, description: 'Returns one timesheet record. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to fetch', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to fetch', required: true)]
#[Rest\Get(path: '/{id}', name: 'get_timesheet', requirements: ['id' => '\d+'])] #[Rest\Get(path: '/{id}', name: 'get_timesheet', requirements: ['id' => '\d+'])]
@@ -273,7 +274,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Creates a new timesheet record * Creates a new timesheet record
*/ */
#[Security("is_granted('create_own_timesheet')")] #[IsGranted('create_own_timesheet')]
#[OA\Post(description: 'Creates a new timesheet record for the current user and returns it afterwards.', responses: [new OA\Response(response: 200, description: 'Returns the new created timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])] #[OA\Post(description: 'Creates a new timesheet record for the current user and returns it afterwards.', responses: [new OA\Response(response: 200, description: 'Returns the new created timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
#[Rest\Post(path: '', name: 'post_timesheet')] #[Rest\Post(path: '', name: 'post_timesheet')]
@@ -328,7 +329,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Update an existing timesheet record * Update an existing timesheet record
*/ */
#[Security("is_granted('edit', timesheet)")] #[IsGranted('edit', 'timesheet')]
#[OA\Patch(description: 'Update an existing timesheet record, you can pass all or just a subset of the attributes.', responses: [new OA\Response(response: 200, description: 'Returns the updated timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])] #[OA\Patch(description: 'Update an existing timesheet record, you can pass all or just a subset of the attributes.', responses: [new OA\Response(response: 200, description: 'Returns the updated timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to update', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to update', required: true)]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
@@ -373,7 +374,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Delete an existing timesheet record * Delete an existing timesheet record
*/ */
#[Security("is_granted('delete', timesheet)")] #[IsGranted('delete', 'timesheet')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet record')])] #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet record')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to delete', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to delete', required: true)]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
@@ -391,7 +392,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Returns the collection of recent user activities * Returns the collection of recent user activities
*/ */
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
#[OA\Response(response: 200, description: 'Returns the collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))] #[OA\Response(response: 200, description: 'Returns the collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
#[Rest\Get(path: '/recent', name: 'recent_timesheet')] #[Rest\Get(path: '/recent', name: 'recent_timesheet')]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
@@ -427,7 +428,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Returns the collection of active timesheet records * Returns the collection of active timesheet records
*/ */
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
#[OA\Response(response: 200, description: 'Returns the collection of active timesheet records for the current user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))] #[OA\Response(response: 200, description: 'Returns the collection of active timesheet records for the current user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
#[Rest\Get(path: '/active', name: 'active_timesheet')] #[Rest\Get(path: '/active', name: 'active_timesheet')]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
@@ -448,7 +449,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Stops an active timesheet record * Stops an active timesheet record
*/ */
#[Security("is_granted('stop', timesheet)")] #[IsGranted('stop', 'timesheet')]
#[OA\Response(response: 200, description: 'Stops an active timesheet record and returns it afterwards.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] #[OA\Response(response: 200, description: 'Stops an active timesheet record and returns it afterwards.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to stop', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to stop', required: true)]
#[Rest\Patch(path: '/{id}/stop', name: 'stop_timesheet', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/stop', name: 'stop_timesheet', requirements: ['id' => '\d+'])]
@@ -467,7 +468,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Restarts a previously stopped timesheet record for the current user * Restarts a previously stopped timesheet record for the current user
*/ */
#[Security("is_granted('start', timesheet)")] #[IsGranted('start', 'timesheet')]
#[OA\Response(response: 200, description: 'Restarts a timesheet record for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] #[OA\Response(response: 200, description: 'Restarts a timesheet record for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)]
#[Rest\Patch(path: '/{id}/restart', name: 'restart_timesheet', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/restart', name: 'restart_timesheet', requirements: ['id' => '\d+'])]
@@ -529,7 +530,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Duplicates an existing timesheet record * Duplicates an existing timesheet record
*/ */
#[Security("is_granted('duplicate', timesheet)")] #[IsGranted('duplicate', 'timesheet')]
#[OA\Response(response: 200, description: 'Duplicates a timesheet record, resetting the export state only.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] #[OA\Response(response: 200, description: 'Duplicates a timesheet record, resetting the export state only.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to duplicate', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to duplicate', required: true)]
#[Rest\Patch(path: '/{id}/duplicate', name: 'duplicate_timesheet', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/duplicate', name: 'duplicate_timesheet', requirements: ['id' => '\d+'])]
@@ -552,7 +553,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Switch the export state of a timesheet record to (un-)lock it * Switch the export state of a timesheet record to (un-)lock it
*/ */
#[Security("is_granted('edit_export', timesheet)")] #[IsGranted('edit_export', 'timesheet')]
#[OA\Response(response: 200, description: 'Switches the exported state on the record and therefor locks / unlocks it for further updates. Needs edit_export_*_timesheet permission.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] #[OA\Response(response: 200, description: 'Switches the exported state on the record and therefor locks / unlocks it for further updates. Needs edit_export_*_timesheet permission.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to switch export state', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to switch export state', required: true)]
#[Rest\Patch(path: '/{id}/export', name: 'export_timesheet', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/export', name: 'export_timesheet', requirements: ['id' => '\d+'])]
@@ -577,7 +578,7 @@ final class TimesheetController extends BaseApiController
/** /**
* Sets the value of a meta-field for an existing timesheet. * Sets the value of a meta-field for an existing timesheet.
*/ */
#[Security("is_granted('edit', timesheet)")] #[IsGranted('edit', 'timesheet')]
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] #[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to set the meta-field value for', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to set the meta-field value for', required: true)]
#[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/meta', requirements: ['id' => '\d+'])]

View File

@@ -23,16 +23,16 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface; use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity; use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/users')] #[Route(path: '/users')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'User')] #[OA\Tag(name: 'User')]
final class UserController extends BaseApiController final class UserController extends BaseApiController
{ {
@@ -51,7 +51,7 @@ final class UserController extends BaseApiController
/** /**
* Returns the collection of users (which are visible to the user) * Returns the collection of users (which are visible to the user)
*/ */
#[Security("is_granted('view_user')")] #[IsGranted('view_user')]
#[OA\Response(response: 200, description: 'Returns the collection of users. Required permission: view_user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/UserCollection')))] #[OA\Response(response: 200, description: 'Returns the collection of users. Required permission: view_user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/UserCollection')))]
#[Rest\Get(path: '', name: 'get_users')] #[Rest\Get(path: '', name: 'get_users')]
#[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiUser')]
@@ -95,7 +95,7 @@ final class UserController extends BaseApiController
/** /**
* Return one user entity * Return one user entity
*/ */
#[Security("is_granted('view', profile)")] #[IsGranted('view', 'profile')]
#[OA\Response(response: 200, description: 'Return one user entity.', content: new OA\JsonContent(ref: '#/components/schemas/UserEntity'))] #[OA\Response(response: 200, description: 'Return one user entity.', content: new OA\JsonContent(ref: '#/components/schemas/UserEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'User ID to fetch', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'User ID to fetch', required: true)]
#[Rest\Get(path: '/{id}', name: 'get_user', requirements: ['id' => '\d+'])] #[Rest\Get(path: '/{id}', name: 'get_user', requirements: ['id' => '\d+'])]
@@ -131,7 +131,7 @@ final class UserController extends BaseApiController
/** /**
* Creates a new user * Creates a new user
*/ */
#[Security("is_granted('create_user')")] #[IsGranted('create_user')]
#[OA\Post(description: 'Creates a new user and returns it afterwards')] #[OA\Post(description: 'Creates a new user and returns it afterwards')]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/UserCreateForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/UserCreateForm'))]
#[Rest\Post(path: '', name: 'post_user')] #[Rest\Post(path: '', name: 'post_user')]
@@ -184,7 +184,7 @@ final class UserController extends BaseApiController
/** /**
* Update an existing user * Update an existing user
*/ */
#[Security("is_granted('edit', profile)")] #[IsGranted('edit', 'profile')]
#[OA\Patch(description: 'Update an existing user, you can pass all or just a subset of all attributes (passing roles will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated user', content: new OA\JsonContent(ref: '#/components/schemas/UserEntity'))])] #[OA\Patch(description: 'Update an existing user, you can pass all or just a subset of all attributes (passing roles will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated user', content: new OA\JsonContent(ref: '#/components/schemas/UserEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/UserEditForm'))] #[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/UserEditForm'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'User ID to update', required: true)] #[OA\Parameter(name: 'id', in: 'path', description: 'User ID to update', required: true)]

View File

@@ -35,18 +35,19 @@ use App\Repository\TeamRepository;
use App\Utils\DataTable; use App\Utils\DataTable;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Exception; use Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to manage activities. * Controller used to manage activities.
*/ */
#[Route(path: '/admin/activity')] #[Route(path: '/admin/activity')]
#[Security("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')")] #[IsGranted(new Expression("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')"))]
final class ActivityController extends AbstractController final class ActivityController extends AbstractController
{ {
public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService) public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService)
@@ -122,7 +123,7 @@ final class ActivityController extends AbstractController
} }
#[Route(path: '/{id}/details', name: 'activity_details', methods: ['GET', 'POST'])] #[Route(path: '/{id}/details', name: 'activity_details', methods: ['GET', 'POST'])]
#[Security("is_granted('view', activity)")] #[IsGranted('view', 'activity')]
public function detailsAction(Activity $activity, TeamRepository $teamRepository, ActivityRateRepository $rateRepository, ActivityStatisticService $statisticService) public function detailsAction(Activity $activity, TeamRepository $teamRepository, ActivityRateRepository $rateRepository, ActivityStatisticService $statisticService)
{ {
$event = new ActivityMetaDefinitionEvent($activity); $event = new ActivityMetaDefinitionEvent($activity);
@@ -172,14 +173,14 @@ final class ActivityController extends AbstractController
} }
#[Route(path: '/{id}/rate/{rate}', name: 'admin_activity_rate_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/rate/{rate}', name: 'admin_activity_rate_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', activity)")] #[IsGranted('edit', 'activity')]
public function editRateAction(Activity $activity, ActivityRate $rate, Request $request, ActivityRateRepository $repository): Response public function editRateAction(Activity $activity, ActivityRate $rate, Request $request, ActivityRateRepository $repository): Response
{ {
return $this->rateFormAction($activity, $rate, $request, $repository, $this->generateUrl('admin_activity_rate_edit', ['id' => $activity->getId(), 'rate' => $rate->getId()])); return $this->rateFormAction($activity, $rate, $request, $repository, $this->generateUrl('admin_activity_rate_edit', ['id' => $activity->getId(), 'rate' => $rate->getId()]));
} }
#[Route(path: '/{id}/rate', name: 'admin_activity_rate_add', methods: ['GET', 'POST'])] #[Route(path: '/{id}/rate', name: 'admin_activity_rate_add', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', activity)")] #[IsGranted('edit', 'activity')]
public function addRateAction(Activity $activity, Request $request, ActivityRateRepository $repository): Response public function addRateAction(Activity $activity, Request $request, ActivityRateRepository $repository): Response
{ {
$rate = new ActivityRate(); $rate = new ActivityRate();
@@ -216,14 +217,14 @@ final class ActivityController extends AbstractController
} }
#[Route(path: '/create/{project}', name: 'admin_activity_create_with_project', methods: ['GET', 'POST'])] #[Route(path: '/create/{project}', name: 'admin_activity_create_with_project', methods: ['GET', 'POST'])]
#[Security("is_granted('create_activity')")] #[IsGranted('create_activity')]
public function createWithProjectAction(Request $request, Project $project): Response public function createWithProjectAction(Request $request, Project $project): Response
{ {
return $this->createActivity($request, $project); return $this->createActivity($request, $project);
} }
#[Route(path: '/create', name: 'admin_activity_create', methods: ['GET', 'POST'])] #[Route(path: '/create', name: 'admin_activity_create', methods: ['GET', 'POST'])]
#[Security("is_granted('create_activity')")] #[IsGranted('create_activity')]
public function createAction(Request $request): Response public function createAction(Request $request): Response
{ {
return $this->createActivity($request, null); return $this->createActivity($request, null);
@@ -258,7 +259,7 @@ final class ActivityController extends AbstractController
} }
#[Route(path: '/{id}/permissions', name: 'admin_activity_permissions', methods: ['GET', 'POST'])] #[Route(path: '/{id}/permissions', name: 'admin_activity_permissions', methods: ['GET', 'POST'])]
#[Security("is_granted('permissions', activity)")] #[IsGranted('permissions', 'activity')]
public function teamPermissionsAction(Activity $activity, Request $request): Response public function teamPermissionsAction(Activity $activity, Request $request): Response
{ {
$form = $this->createForm(ActivityTeamPermissionForm::class, $activity, [ $form = $this->createForm(ActivityTeamPermissionForm::class, $activity, [
@@ -291,7 +292,8 @@ final class ActivityController extends AbstractController
} }
#[Route(path: '/{id}/create_team', name: 'activity_team_create', methods: ['GET'])] #[Route(path: '/{id}/create_team', name: 'activity_team_create', methods: ['GET'])]
#[Security("is_granted('create_team') and is_granted('permissions', activity)")] #[IsGranted('create_team')]
#[IsGranted('permissions', 'activity')]
public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository): Response public function createDefaultTeamAction(Activity $activity, TeamRepository $teamRepository): Response
{ {
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]); $defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
@@ -315,7 +317,7 @@ final class ActivityController extends AbstractController
} }
#[Route(path: '/{id}/edit', name: 'admin_activity_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/edit', name: 'admin_activity_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', activity)")] #[IsGranted('edit', 'activity')]
public function editAction(Activity $activity, Request $request): Response public function editAction(Activity $activity, Request $request): Response
{ {
$event = new ActivityMetaDefinitionEvent($activity); $event = new ActivityMetaDefinitionEvent($activity);
@@ -343,7 +345,7 @@ final class ActivityController extends AbstractController
} }
#[Route(path: '/{id}/delete', name: 'admin_activity_delete', methods: ['GET', 'POST'])] #[Route(path: '/{id}/delete', name: 'admin_activity_delete', methods: ['GET', 'POST'])]
#[Security("is_granted('delete', activity)")] #[IsGranted('delete', 'activity')]
public function deleteAction(Activity $activity, Request $request, ActivityStatisticService $statisticService): Response public function deleteAction(Activity $activity, Request $request, ActivityStatisticService $statisticService): Response
{ {
$stats = $statisticService->getActivityStatistics($activity); $stats = $statisticService->getActivityStatistics($activity);

View File

@@ -12,10 +12,10 @@ namespace App\Controller\Auth;
use App\Configuration\SamlConfigurationInterface; use App\Configuration\SamlConfigurationInterface;
use App\Saml\SamlAuthFactory; use App\Saml\SamlAuthFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
#[Route(path: '/saml')] #[Route(path: '/saml')]
final class SamlController extends AbstractController final class SamlController extends AbstractController

View File

@@ -15,16 +15,16 @@ use App\Entity\User;
use App\Form\CalendarForm; use App\Form\CalendarForm;
use App\Timesheet\TrackingModeService; use App\Timesheet\TrackingModeService;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to display calendars. * Controller used to display calendars.
*/ */
#[Route(path: '/calendar')] #[Route(path: '/calendar')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
final class CalendarController extends AbstractController final class CalendarController extends AbstractController
{ {
public function __construct(private CalendarService $calendarService, private SystemConfiguration $configuration, private TrackingModeService $service) public function __construct(private CalendarService $calendarService, private SystemConfiguration $configuration, private TrackingModeService $service)

View File

@@ -38,8 +38,8 @@ use App\Utils\DataTable;
use App\Utils\FileHelper; use App\Utils\FileHelper;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use JeroenDesloovere\VCard\VCard; use JeroenDesloovere\VCard\VCard;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -48,12 +48,13 @@ use Symfony\Component\Intl\Countries;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to manage customer in the admin part of the site. * Controller used to manage customer in the admin part of the site.
*/ */
#[Route(path: '/admin/customer')] #[Route(path: '/admin/customer')]
#[Security("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')")] #[IsGranted(new Expression("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')"))]
final class CustomerController extends AbstractController final class CustomerController extends AbstractController
{ {
public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher) public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher)
@@ -139,7 +140,7 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/create', name: 'admin_customer_create', methods: ['GET', 'POST'])] #[Route(path: '/create', name: 'admin_customer_create', methods: ['GET', 'POST'])]
#[Security("is_granted('create_customer')")] #[IsGranted('create_customer')]
public function createAction(Request $request, CustomerService $customerService) public function createAction(Request $request, CustomerService $customerService)
{ {
$customer = $customerService->createNewCustomer(''); $customer = $customerService->createNewCustomer('');
@@ -148,7 +149,7 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/permissions', name: 'admin_customer_permissions', methods: ['GET', 'POST'])] #[Route(path: '/{id}/permissions', name: 'admin_customer_permissions', methods: ['GET', 'POST'])]
#[Security("is_granted('permissions', customer)")] #[IsGranted('permissions', 'customer')]
public function teamPermissionsAction(Customer $customer, Request $request) public function teamPermissionsAction(Customer $customer, Request $request)
{ {
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [ $form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
@@ -181,7 +182,7 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/comment_delete/{token}', name: 'customer_comment_delete', methods: ['GET'])] #[Route(path: '/{id}/comment_delete/{token}', name: 'customer_comment_delete', methods: ['GET'])]
#[Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")] #[IsGranted(new Expression("is_granted('edit', subject.getCustomer()) and is_granted('comments', subject.getCustomer())"), 'comment')]
public function deleteCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager) public function deleteCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
{ {
$customerId = $comment->getCustomer()->getId(); $customerId = $comment->getCustomer()->getId();
@@ -204,7 +205,7 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/comment_add', name: 'customer_comment_add', methods: ['POST'])] #[Route(path: '/{id}/comment_add', name: 'customer_comment_add', methods: ['POST'])]
#[Security("is_granted('comments', customer)")] #[IsGranted('comments', 'customer')]
public function addCommentAction(Customer $customer, Request $request) public function addCommentAction(Customer $customer, Request $request)
{ {
$comment = new CustomerComment($customer); $comment = new CustomerComment($customer);
@@ -224,7 +225,7 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/comment_pin/{token}', name: 'customer_comment_pin', methods: ['GET'])] #[Route(path: '/{id}/comment_pin/{token}', name: 'customer_comment_pin', methods: ['GET'])]
#[Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")] #[IsGranted(new Expression("is_granted('edit', subject.getCustomer()) and is_granted('comments', subject.getCustomer())"), 'comment')]
public function pinCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager) public function pinCommentAction(CustomerComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
{ {
$customerId = $comment->getCustomer()->getId(); $customerId = $comment->getCustomer()->getId();
@@ -248,7 +249,8 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/create_team', name: 'customer_team_create', methods: ['GET'])] #[Route(path: '/{id}/create_team', name: 'customer_team_create', methods: ['GET'])]
#[Security("is_granted('create_team') and is_granted('permissions', customer)")] #[IsGranted('create_team')]
#[IsGranted('permissions', 'customer')]
public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository) public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository)
{ {
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]); $defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
@@ -272,7 +274,7 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/projects/{page}', defaults: ['page' => 1], name: 'customer_projects', methods: ['GET', 'POST'])] #[Route(path: '/{id}/projects/{page}', defaults: ['page' => 1], name: 'customer_projects', methods: ['GET', 'POST'])]
#[Security("is_granted('view', customer)")] #[IsGranted('view', 'customer')]
public function projectsAction(Customer $customer, int $page, ProjectRepository $projectRepository) public function projectsAction(Customer $customer, int $page, ProjectRepository $projectRepository)
{ {
$query = new ProjectQuery(); $query = new ProjectQuery();
@@ -295,7 +297,7 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/details', name: 'customer_details', methods: ['GET', 'POST'])] #[Route(path: '/{id}/details', name: 'customer_details', methods: ['GET', 'POST'])]
#[Security("is_granted('view', customer)")] #[IsGranted('view', 'customer')]
public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository, CustomerStatisticService $statisticService) public function detailsAction(Customer $customer, TeamRepository $teamRepository, CustomerRateRepository $rateRepository, CustomerStatisticService $statisticService)
{ {
$event = new CustomerMetaDefinitionEvent($customer); $event = new CustomerMetaDefinitionEvent($customer);
@@ -362,7 +364,7 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/vcard', name: 'customer_vcard', methods: ['GET'])] #[Route(path: '/{id}/vcard', name: 'customer_vcard', methods: ['GET'])]
#[Security("is_granted('view', customer)")] #[IsGranted('view', 'customer')]
public function downloadVCard(Customer $customer): Response public function downloadVCard(Customer $customer): Response
{ {
$vcard = new VCard(); $vcard = new VCard();
@@ -415,14 +417,14 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/rate/{rate}', name: 'admin_customer_rate_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/rate/{rate}', name: 'admin_customer_rate_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', customer)")] #[IsGranted('edit', 'customer')]
public function editRateAction(Customer $customer, CustomerRate $rate, Request $request, CustomerRateRepository $repository): Response public function editRateAction(Customer $customer, CustomerRate $rate, Request $request, CustomerRateRepository $repository): Response
{ {
return $this->rateFormAction($customer, $rate, $request, $repository, $this->generateUrl('admin_customer_rate_edit', ['id' => $customer->getId(), 'rate' => $rate->getId()])); return $this->rateFormAction($customer, $rate, $request, $repository, $this->generateUrl('admin_customer_rate_edit', ['id' => $customer->getId(), 'rate' => $rate->getId()]));
} }
#[Route(path: '/{id}/rate', name: 'admin_customer_rate_add', methods: ['GET', 'POST'])] #[Route(path: '/{id}/rate', name: 'admin_customer_rate_add', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', customer)")] #[IsGranted('edit', 'customer')]
public function addRateAction(Customer $customer, Request $request, CustomerRateRepository $repository): Response public function addRateAction(Customer $customer, Request $request, CustomerRateRepository $repository): Response
{ {
$rate = new CustomerRate(); $rate = new CustomerRate();
@@ -459,14 +461,14 @@ final class CustomerController extends AbstractController
} }
#[Route(path: '/{id}/edit', name: 'admin_customer_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/edit', name: 'admin_customer_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', customer)")] #[IsGranted('edit', 'customer')]
public function editAction(Customer $customer, Request $request) public function editAction(Customer $customer, Request $request)
{ {
return $this->renderCustomerForm($customer, $request); return $this->renderCustomerForm($customer, $request);
} }
#[Route(path: '/{id}/delete', name: 'admin_customer_delete', methods: ['GET', 'POST'])] #[Route(path: '/{id}/delete', name: 'admin_customer_delete', methods: ['GET', 'POST'])]
#[Security("is_granted('delete', customer)")] #[IsGranted('delete', 'customer')]
public function deleteAction(Customer $customer, Request $request, CustomerStatisticService $statisticService) public function deleteAction(Customer $customer, Request $request, CustomerStatisticService $statisticService)
{ {
$stats = $statisticService->getCustomerStatistics($customer); $stats = $statisticService->getCustomerStatistics($customer);

View File

@@ -16,19 +16,19 @@ use App\Repository\BookmarkRepository;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
use App\Widget\WidgetService; use App\Widget\WidgetService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Dashboard controller for the admin area. * Dashboard controller for the admin area.
*/ */
#[Route(path: '/dashboard')] #[Route(path: '/dashboard')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
final class DashboardController extends AbstractController final class DashboardController extends AbstractController
{ {
public const BOOKMARK_TYPE = 'dashboard'; public const BOOKMARK_TYPE = 'dashboard';

View File

@@ -13,16 +13,16 @@ use App\Utils\FileHelper;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use App\Utils\ReleaseVersion; use App\Utils\ReleaseVersion;
use Composer\InstalledVersions; use Composer\InstalledVersions;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
#[Route(path: '/doctor')] #[Route(path: '/doctor')]
#[Security("is_granted('system_information')")] #[IsGranted('system_information')]
final class DoctorController extends AbstractController final class DoctorController extends AbstractController
{ {
/** /**
@@ -51,7 +51,7 @@ final class DoctorController extends AbstractController
} }
#[Route(path: '/flush-log/{token}', name: 'doctor_flush_log', methods: ['GET'])] #[Route(path: '/flush-log/{token}', name: 'doctor_flush_log', methods: ['GET'])]
#[Security("is_granted('system_configuration')")] #[IsGranted('system_configuration')]
public function deleteLogfileAction(string $token, CsrfTokenManagerInterface $csrfTokenManager): Response public function deleteLogfileAction(string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$csrfTokenManager->isTokenValid(new CsrfToken('doctor.flush_log', $token))) { if (!$csrfTokenManager->isTokenValid(new CsrfToken('doctor.flush_log', $token))) {

View File

@@ -16,17 +16,17 @@ use App\Export\TooManyItemsExportException;
use App\Form\Toolbar\ExportToolbarForm; use App\Form\Toolbar\ExportToolbarForm;
use App\Repository\Query\ExportQuery; use App\Repository\Query\ExportQuery;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to export timesheet data. * Controller used to export timesheet data.
*/ */
#[Route(path: '/export')] #[Route(path: '/export')]
#[Security("is_granted('create_export')")] #[IsGranted('create_export')]
final class ExportController extends AbstractController final class ExportController extends AbstractController
{ {
public function __construct(private ServiceExport $export) public function __construct(private ServiceExport $export)

View File

@@ -11,23 +11,23 @@ namespace App\Controller;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use App\Timesheet\FavoriteRecordService; use App\Timesheet\FavoriteRecordService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/favorite')] #[Route(path: '/favorite')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
final class FavoriteController extends AbstractController final class FavoriteController extends AbstractController
{ {
#[Route(path: '/timesheet/', name: 'favorites_timesheets', methods: ['GET'])] #[Route(path: '/timesheet/', name: 'favorites_timesheets', methods: ['GET'])]
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
public function favoriteAction(): Response public function favoriteAction(): Response
{ {
return $this->render('partials/recent-activities.html.twig'); return $this->render('partials/recent-activities.html.twig');
} }
#[Route(path: '/timesheet/add/{id}', name: 'favorites_timesheets_add', methods: ['GET'])] #[Route(path: '/timesheet/add/{id}', name: 'favorites_timesheets_add', methods: ['GET'])]
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
public function add(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response public function add(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
{ {
$favoriteRecordService->addFavorite($timesheet); $favoriteRecordService->addFavorite($timesheet);
@@ -36,7 +36,7 @@ final class FavoriteController extends AbstractController
} }
#[Route(path: '/timesheet/remove/{id}', name: 'favorites_timesheets_remove', methods: ['GET'])] #[Route(path: '/timesheet/remove/{id}', name: 'favorites_timesheets_remove', methods: ['GET'])]
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
public function remove(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response public function remove(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
{ {
$favoriteRecordService->removeFavorite($timesheet); $favoriteRecordService->removeFavorite($timesheet);

View File

@@ -11,16 +11,16 @@ namespace App\Controller;
use App\Configuration\LocaleService; use App\Configuration\LocaleService;
use App\Entity\User; use App\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Homepage controller is a redirect controller with user specific logic. * Homepage controller is a redirect controller with user specific logic.
*/ */
#[Route(path: '/homepage')] #[Route(path: '/homepage')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
final class HomepageController extends AbstractController final class HomepageController extends AbstractController
{ {
public const DEFAULT_ROUTE = 'timesheet'; public const DEFAULT_ROUTE = 'timesheet';

View File

@@ -37,7 +37,7 @@ use App\Repository\Query\InvoiceQuery;
use App\Utils\DataTable; use App\Utils\DataTable;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Exception; use Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -46,13 +46,15 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/** /**
* Controller used to create invoices and manage invoice templates. * Controller used to create invoices and manage invoice templates.
*/ */
#[Route(path: '/invoice')] #[Route(path: '/invoice')]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('view_invoice')")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('view_invoice')]
final class InvoiceController extends AbstractController final class InvoiceController extends AbstractController
{ {
public function __construct( public function __construct(
@@ -64,7 +66,7 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/', name: 'invoice', methods: ['GET', 'POST'])] #[Route(path: '/', name: 'invoice', methods: ['GET', 'POST'])]
#[Security("is_granted('create_invoice')")] #[IsGranted('create_invoice')]
public function indexAction(Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response public function indexAction(Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$this->templateRepository->hasTemplate()) { if (!$this->templateRepository->hasTemplate()) {
@@ -127,7 +129,8 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/preview/{customer}/{token}', name: 'invoice_preview', methods: ['GET'])] #[Route(path: '/preview/{customer}/{token}', name: 'invoice_preview', methods: ['GET'])]
#[Security("is_granted('access', customer) and is_granted('create_invoice')")] #[IsGranted('create_invoice')]
#[IsGranted('access', 'customer')]
public function previewAction(Customer $customer, string $token, Request $request): Response public function previewAction(Customer $customer, string $token, Request $request): Response
{ {
if (!$this->templateRepository->hasTemplate()) { if (!$this->templateRepository->hasTemplate()) {
@@ -167,7 +170,8 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/save-invoice/{customer}/{token}', name: 'invoice_create', methods: ['GET'])] #[Route(path: '/save-invoice/{customer}/{token}', name: 'invoice_create', methods: ['GET'])]
#[Security("is_granted('access', customer) and is_granted('create_invoice')")] #[IsGranted('create_invoice')]
#[IsGranted('access', 'customer')]
public function createInvoiceAction(Customer $customer, string $token, Request $request, CustomerRepository $customerRepository): Response public function createInvoiceAction(Customer $customer, string $token, Request $request, CustomerRepository $customerRepository): Response
{ {
if (!$this->templateRepository->hasTemplate()) { if (!$this->templateRepository->hasTemplate()) {
@@ -214,7 +218,8 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/change-status/{id}/{status}/{token}', name: 'admin_invoice_status', methods: ['GET', 'POST'])] #[Route(path: '/change-status/{id}/{status}/{token}', name: 'admin_invoice_status', methods: ['GET', 'POST'])]
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('create_invoice')")] #[IsGranted('create_invoice')]
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
public function changeStatusAction(Invoice $invoice, string $status, string $token, Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response public function changeStatusAction(Invoice $invoice, string $status, string $token, Request $request, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) { if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
@@ -250,7 +255,8 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/edit/{id}', name: 'admin_invoice_edit', methods: ['GET', 'POST'])] #[Route(path: '/edit/{id}', name: 'admin_invoice_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('create_invoice')")] #[IsGranted('create_invoice')]
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
public function editAction(Invoice $invoice, Request $request): Response public function editAction(Invoice $invoice, Request $request): Response
{ {
$form = $this->createInvoiceEditForm($invoice); $form = $this->createInvoiceEditForm($invoice);
@@ -275,7 +281,8 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/delete/{id}/{token}', name: 'admin_invoice_delete', methods: ['GET'])] #[Route(path: '/delete/{id}/{token}', name: 'admin_invoice_delete', methods: ['GET'])]
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('delete_invoice')")] #[IsGranted('delete_invoice')]
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
public function deleteInvoiceAction(Invoice $invoice, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response public function deleteInvoiceAction(Invoice $invoice, string $token, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) { if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.status', $token))) {
@@ -297,7 +304,8 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/download/{id}', name: 'admin_invoice_download', methods: ['GET'])] #[Route(path: '/download/{id}', name: 'admin_invoice_download', methods: ['GET'])]
#[Security("is_granted('access', invoice.getCustomer()) and is_granted('view_invoice')")] #[IsGranted('view_invoice')]
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
public function downloadAction(Invoice $invoice): Response public function downloadAction(Invoice $invoice): Response
{ {
$file = $this->service->getInvoiceFile($invoice); $file = $this->service->getInvoiceFile($invoice);
@@ -312,7 +320,7 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/show/{page}', defaults: ['page' => 1], requirements: ['page' => '[1-9]\d*'], name: 'admin_invoice_list', methods: ['GET'])] #[Route(path: '/show/{page}', defaults: ['page' => 1], requirements: ['page' => '[1-9]\d*'], name: 'admin_invoice_list', methods: ['GET'])]
#[Security("is_granted('view_invoice')")] #[IsGranted('view_invoice')]
public function showInvoicesAction(Request $request, int $page): Response public function showInvoicesAction(Request $request, int $page): Response
{ {
$invoice = null; $invoice = null;
@@ -371,7 +379,7 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/export', name: 'invoice_export', methods: ['GET'])] #[Route(path: '/export', name: 'invoice_export', methods: ['GET'])]
#[Security("is_granted('view_invoice')")] #[IsGranted('view_invoice')]
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter) public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
{ {
$query = new InvoiceArchiveQuery(); $query = new InvoiceArchiveQuery();
@@ -394,7 +402,7 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/template/{page}', requirements: ['page' => '[1-9]\d*'], defaults: ['page' => 1], name: 'admin_invoice_template', methods: ['GET', 'POST'])] #[Route(path: '/template/{page}', requirements: ['page' => '[1-9]\d*'], defaults: ['page' => 1], name: 'admin_invoice_template', methods: ['GET', 'POST'])]
#[Security("is_granted('manage_invoice_template')")] #[IsGranted('manage_invoice_template')]
public function listTemplateAction(int $page): Response public function listTemplateAction(int $page): Response
{ {
$query = new BaseQuery(); $query = new BaseQuery();
@@ -431,14 +439,14 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/template/{id}/edit', name: 'admin_invoice_template_edit', methods: ['GET', 'POST'])] #[Route(path: '/template/{id}/edit', name: 'admin_invoice_template_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('manage_invoice_template')")] #[IsGranted('manage_invoice_template')]
public function editTemplateAction(InvoiceTemplate $template, Request $request): Response public function editTemplateAction(InvoiceTemplate $template, Request $request): Response
{ {
return $this->renderTemplateForm($template, $request); return $this->renderTemplateForm($template, $request);
} }
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])] #[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
#[Security("is_granted('upload_invoice_template')")] #[IsGranted('upload_invoice_template')]
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository) public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository)
{ {
$dir = $documentRepository->getUploadDirectory(); $dir = $documentRepository->getUploadDirectory();
@@ -538,7 +546,7 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/document/{id}/delete/{token}', name: 'invoice_document_delete', methods: ['GET', 'POST'])] #[Route(path: '/document/{id}/delete/{token}', name: 'invoice_document_delete', methods: ['GET', 'POST'])]
#[Security("is_granted('manage_invoice_template')")] #[IsGranted('manage_invoice_template')]
public function deleteDocument(string $id, string $token, CsrfTokenManagerInterface $csrfTokenManager, InvoiceDocumentRepository $documentRepository): Response public function deleteDocument(string $id, string $token, CsrfTokenManagerInterface $csrfTokenManager, InvoiceDocumentRepository $documentRepository): Response
{ {
$document = $documentRepository->findByName($id); $document = $documentRepository->findByName($id);
@@ -581,14 +589,14 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/template/create/{id}', name: 'admin_invoice_template_copy', methods: ['GET', 'POST'])] #[Route(path: '/template/create/{id}', name: 'admin_invoice_template_copy', methods: ['GET', 'POST'])]
#[Security("is_granted('manage_invoice_template')")] #[IsGranted('manage_invoice_template')]
public function copyTemplateAction(Request $request, InvoiceTemplate $copyFrom): Response public function copyTemplateAction(Request $request, InvoiceTemplate $copyFrom): Response
{ {
return $this->createTemplate($request, $copyFrom); return $this->createTemplate($request, $copyFrom);
} }
#[Route(path: '/template/create', name: 'admin_invoice_template_create', methods: ['GET', 'POST'])] #[Route(path: '/template/create', name: 'admin_invoice_template_create', methods: ['GET', 'POST'])]
#[Security("is_granted('manage_invoice_template')")] #[IsGranted('manage_invoice_template')]
public function createTemplateAction(Request $request): Response public function createTemplateAction(Request $request): Response
{ {
return $this->createTemplate($request, null); return $this->createTemplate($request, null);
@@ -608,7 +616,7 @@ final class InvoiceController extends AbstractController
} }
#[Route(path: '/template/{id}/delete/{csrfToken}', name: 'admin_invoice_template_delete', methods: ['GET', 'POST'])] #[Route(path: '/template/{id}/delete/{csrfToken}', name: 'admin_invoice_template_delete', methods: ['GET', 'POST'])]
#[Security("is_granted('manage_invoice_template')")] #[IsGranted('manage_invoice_template')]
public function deleteTemplate(InvoiceTemplate $template, string $csrfToken, CsrfTokenManagerInterface $csrfTokenManager): Response public function deleteTemplate(InvoiceTemplate $template, string $csrfToken, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete_template', $csrfToken))) { if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete_template', $csrfToken))) {

View File

@@ -22,19 +22,20 @@ use App\Repository\UserRepository;
use App\Security\RolePermissionManager; use App\Security\RolePermissionManager;
use App\Security\RoleService; use App\Security\RoleService;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to manage user roles and role permissions. * Controller used to manage user roles and role permissions.
*/ */
#[Route(path: '/admin/permissions')] #[Route(path: '/admin/permissions')]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('role_permissions')")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('role_permissions')]
final class PermissionController extends AbstractController final class PermissionController extends AbstractController
{ {
public const TOKEN_NAME = 'user_role_permissions'; public const TOKEN_NAME = 'user_role_permissions';
@@ -44,7 +45,7 @@ final class PermissionController extends AbstractController
} }
#[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])] #[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])]
#[Security("is_granted('role_permissions')")] #[IsGranted('role_permissions')]
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService) public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService)
{ {
$all = $this->roleRepository->findAll(); $all = $this->roleRepository->findAll();
@@ -161,7 +162,7 @@ final class PermissionController extends AbstractController
} }
#[Route(path: '/roles/create', name: 'admin_user_roles', methods: ['GET', 'POST'])] #[Route(path: '/roles/create', name: 'admin_user_roles', methods: ['GET', 'POST'])]
#[Security("is_granted('role_permissions')")] #[IsGranted('role_permissions')]
public function createRole(Request $request): Response public function createRole(Request $request): Response
{ {
$role = new Role(); $role = new Role();
@@ -195,7 +196,7 @@ final class PermissionController extends AbstractController
} }
#[Route(path: '/roles/{id}/delete/{csrfToken}', name: 'admin_user_role_delete', methods: ['GET', 'POST'])] #[Route(path: '/roles/{id}/delete/{csrfToken}', name: 'admin_user_role_delete', methods: ['GET', 'POST'])]
#[Security("is_granted('role_permissions')")] #[IsGranted('role_permissions')]
public function deleteRole(Role $role, string $csrfToken, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response public function deleteRole(Role $role, string $csrfToken, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) { if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {
@@ -225,7 +226,7 @@ final class PermissionController extends AbstractController
} }
#[Route(path: '/roles/{id}/{name}/{value}/{csrfToken}', name: 'admin_user_permission_save', methods: ['POST'])] #[Route(path: '/roles/{id}/{name}/{value}/{csrfToken}', name: 'admin_user_permission_save', methods: ['POST'])]
#[Security("is_granted('role_permissions')")] #[IsGranted('role_permissions')]
public function savePermission(Role $role, string $name, bool $value, string $csrfToken, RolePermissionRepository $rolePermissionRepository, CsrfTokenManagerInterface $csrfTokenManager): Response public function savePermission(Role $role, string $name, bool $value, string $csrfToken, RolePermissionRepository $rolePermissionRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
{ {
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) { if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {

View File

@@ -11,15 +11,15 @@ namespace App\Controller;
use App\Plugin\PluginManager; use App\Plugin\PluginManager;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
#[Route(path: '/admin/plugins')] #[Route(path: '/admin/plugins')]
#[Security("is_granted('plugins')")] #[IsGranted('plugins')]
final class PluginController extends AbstractController final class PluginController extends AbstractController
{ {
#[Route(path: '/', name: 'plugins', methods: ['GET'])] #[Route(path: '/', name: 'plugins', methods: ['GET'])]

View File

@@ -32,18 +32,19 @@ use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin; use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter; use Endroid\QrCode\Writer\PngWriter;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface; use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* User profile controller * User profile controller
*/ */
#[Route(path: '/profile')] #[Route(path: '/profile')]
#[Security("(is_granted('view_own_profile') or is_granted('view_other_profile'))")] #[IsGranted(new Expression("is_granted('view_own_profile') or is_granted('view_other_profile')"))]
final class ProfileController extends AbstractController final class ProfileController extends AbstractController
{ {
#[Route(path: '/', name: 'my_profile', methods: ['GET'])] #[Route(path: '/', name: 'my_profile', methods: ['GET'])]
@@ -53,7 +54,7 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}', name: 'user_profile', methods: ['GET'])] #[Route(path: '/{username}', name: 'user_profile', methods: ['GET'])]
#[Security("is_granted('view', profile)")] #[IsGranted('view', 'profile')]
public function indexAction(User $profile, TimesheetRepository $repository, TimesheetStatisticService $statisticService): Response public function indexAction(User $profile, TimesheetRepository $repository, TimesheetStatisticService $statisticService): Response
{ {
$dateFactory = $this->getDateTimeFactory(); $dateFactory = $this->getDateTimeFactory();
@@ -79,7 +80,7 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/edit', name: 'user_profile_edit', methods: ['GET', 'POST'])] #[Route(path: '/{username}/edit', name: 'user_profile_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', profile)")] #[IsGranted('edit', 'profile')]
public function editAction(User $profile, Request $request, UserRepository $userRepository): Response public function editAction(User $profile, Request $request, UserRepository $userRepository): Response
{ {
$form = $this->createEditForm($profile); $form = $this->createEditForm($profile);
@@ -101,7 +102,8 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/password', name: 'user_profile_password', methods: ['GET', 'POST'])] #[Route(path: '/{username}/password', name: 'user_profile_password', methods: ['GET', 'POST'])]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('password', profile)")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('password', 'profile')]
public function passwordAction(User $profile, Request $request, UserService $userService): Response public function passwordAction(User $profile, Request $request, UserService $userService): Response
{ {
$form = $this->createPasswordForm($profile); $form = $this->createPasswordForm($profile);
@@ -123,7 +125,8 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/api-token', name: 'user_profile_api_token', methods: ['GET', 'POST'])] #[Route(path: '/{username}/api-token', name: 'user_profile_api_token', methods: ['GET', 'POST'])]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('api-token', profile)")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('api-token', 'profile')]
public function apiTokenAction(User $profile, Request $request, UserService $userService): Response public function apiTokenAction(User $profile, Request $request, UserService $userService): Response
{ {
$form = $this->createApiTokenForm($profile); $form = $this->createApiTokenForm($profile);
@@ -145,7 +148,8 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/roles', name: 'user_profile_roles', methods: ['GET', 'POST'])] #[Route(path: '/{username}/roles', name: 'user_profile_roles', methods: ['GET', 'POST'])]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('roles', profile)")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('roles', 'profile')]
public function rolesAction(User $profile, Request $request, UserRepository $userRepository): Response public function rolesAction(User $profile, Request $request, UserRepository $userRepository): Response
{ {
$isSuperAdmin = $profile->isSuperAdmin(); $isSuperAdmin = $profile->isSuperAdmin();
@@ -175,7 +179,7 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/teams', name: 'user_profile_teams', methods: ['GET', 'POST'])] #[Route(path: '/{username}/teams', name: 'user_profile_teams', methods: ['GET', 'POST'])]
#[Security("is_granted('teams', profile)")] #[IsGranted('teams', 'profile')]
public function teamsAction(User $profile, Request $request, UserRepository $userRepository, TeamRepository $teamRepository): Response public function teamsAction(User $profile, Request $request, UserRepository $userRepository, TeamRepository $teamRepository): Response
{ {
$originalMembers = new ArrayCollection(); $originalMembers = new ArrayCollection();
@@ -210,7 +214,7 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/prefs', name: 'user_profile_preferences', methods: ['GET', 'POST'])] #[Route(path: '/{username}/prefs', name: 'user_profile_preferences', methods: ['GET', 'POST'])]
#[Security("is_granted('preferences', profile)")] #[IsGranted('preferences', 'profile')]
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher, UserRepository $userRepository): Response public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher, UserRepository $userRepository): Response
{ {
// we need to prepare the user preferences, which is done via an EventSubscriber // we need to prepare the user preferences, which is done via an EventSubscriber
@@ -336,7 +340,8 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/2fa', name: 'user_profile_2fa', methods: ['GET', 'POST'])] #[Route(path: '/{username}/2fa', name: 'user_profile_2fa', methods: ['GET', 'POST'])]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('2fa', profile)")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('2fa', 'profile')]
public function twoFactorAction(User $profile, Request $request, UserService $userService, TotpAuthenticatorInterface $totpAuthenticator): Response public function twoFactorAction(User $profile, Request $request, UserService $userService, TotpAuthenticatorInterface $totpAuthenticator): Response
{ {
if (!$profile->hasTotpSecret()) { if (!$profile->hasTotpSecret()) {
@@ -382,7 +387,8 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/2fa_deactivate', name: 'user_profile_2fa_deactivate', methods: ['POST'])] #[Route(path: '/{username}/2fa_deactivate', name: 'user_profile_2fa_deactivate', methods: ['POST'])]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('2fa', profile)")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('2fa', 'profile')]
public function deactivateTwoFactorAction(User $profile, Request $request, UserService $userService, TotpAuthenticatorInterface $totpAuthenticator): Response public function deactivateTwoFactorAction(User $profile, Request $request, UserService $userService, TotpAuthenticatorInterface $totpAuthenticator): Response
{ {
if ($profile->hasTotpSecret()) { if ($profile->hasTotpSecret()) {
@@ -401,7 +407,7 @@ final class ProfileController extends AbstractController
} }
#[Route(path: '/{username}/totp.png', name: 'user_profile_2fa_image', methods: ['GET'])] #[Route(path: '/{username}/totp.png', name: 'user_profile_2fa_image', methods: ['GET'])]
#[Security("is_granted('2fa', profile)")] #[IsGranted('2fa', 'profile')]
public function displayTotpQrCode(User $profile, TotpAuthenticatorInterface $totpAuthenticator): Response public function displayTotpQrCode(User $profile, TotpAuthenticatorInterface $totpAuthenticator): Response
{ {
if (!$profile->hasTotpSecret()) { if (!$profile->hasTotpSecret()) {

View File

@@ -40,20 +40,21 @@ use App\Repository\TeamRepository;
use App\Utils\Context; use App\Utils\Context;
use App\Utils\DataTable; use App\Utils\DataTable;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to manage projects. * Controller used to manage projects.
*/ */
#[Route(path: '/admin/project')] #[Route(path: '/admin/project')]
#[Security("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')")] #[IsGranted(new Expression("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')"))]
final class ProjectController extends AbstractController final class ProjectController extends AbstractController
{ {
public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService) public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService)
@@ -132,7 +133,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/permissions', name: 'admin_project_permissions', methods: ['GET', 'POST'])] #[Route(path: '/{id}/permissions', name: 'admin_project_permissions', methods: ['GET', 'POST'])]
#[Security("is_granted('permissions', project)")] #[IsGranted('permissions', 'project')]
public function teamPermissions(Project $project, Request $request) public function teamPermissions(Project $project, Request $request)
{ {
$form = $this->createForm(ProjectTeamPermissionForm::class, $project, [ $form = $this->createForm(ProjectTeamPermissionForm::class, $project, [
@@ -165,14 +166,14 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/create/{customer}', name: 'admin_project_create_with_customer', methods: ['GET', 'POST'])] #[Route(path: '/create/{customer}', name: 'admin_project_create_with_customer', methods: ['GET', 'POST'])]
#[Security("is_granted('create_project')")] #[IsGranted('create_project')]
public function createWithCustomerAction(Request $request, Customer $customer) public function createWithCustomerAction(Request $request, Customer $customer)
{ {
return $this->createProject($request, $customer); return $this->createProject($request, $customer);
} }
#[Route(path: '/create', name: 'admin_project_create', methods: ['GET', 'POST'])] #[Route(path: '/create', name: 'admin_project_create', methods: ['GET', 'POST'])]
#[Security("is_granted('create_project')")] #[IsGranted('create_project')]
public function createAction(Request $request) public function createAction(Request $request)
{ {
return $this->createProject($request, null); return $this->createProject($request, null);
@@ -204,7 +205,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/comment_delete/{token}', name: 'project_comment_delete', methods: ['GET'])] #[Route(path: '/{id}/comment_delete/{token}', name: 'project_comment_delete', methods: ['GET'])]
#[Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")] #[IsGranted(new Expression("is_granted('edit', subject.getProject()) and is_granted('comments', subject.getProject())"), 'comment')]
public function deleteCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager) public function deleteCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
{ {
$projectId = $comment->getProject()->getId(); $projectId = $comment->getProject()->getId();
@@ -227,7 +228,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/comment_add', name: 'project_comment_add', methods: ['POST'])] #[Route(path: '/{id}/comment_add', name: 'project_comment_add', methods: ['POST'])]
#[Security("is_granted('comments', project)")] #[IsGranted('comments', 'project')]
public function addCommentAction(Project $project, Request $request) public function addCommentAction(Project $project, Request $request)
{ {
$comment = new ProjectComment($project); $comment = new ProjectComment($project);
@@ -247,7 +248,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/comment_pin/{token}', name: 'project_comment_pin', methods: ['GET'])] #[Route(path: '/{id}/comment_pin/{token}', name: 'project_comment_pin', methods: ['GET'])]
#[Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")] #[IsGranted(new Expression("is_granted('edit', subject.getProject()) and is_granted('comments', subject.getProject())"), 'comment')]
public function pinCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager) public function pinCommentAction(ProjectComment $comment, string $token, CsrfTokenManagerInterface $csrfTokenManager)
{ {
$projectId = $comment->getProject()->getId(); $projectId = $comment->getProject()->getId();
@@ -271,7 +272,8 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/create_team', name: 'project_team_create', methods: ['GET'])] #[Route(path: '/{id}/create_team', name: 'project_team_create', methods: ['GET'])]
#[Security("is_granted('create_team') and is_granted('permissions', project)")] #[IsGranted('create_team')]
#[IsGranted('permissions', 'project')]
public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository) public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository)
{ {
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]); $defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
@@ -295,7 +297,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/activities/{page}', defaults: ['page' => 1], name: 'project_activities', methods: ['GET', 'POST'])] #[Route(path: '/{id}/activities/{page}', defaults: ['page' => 1], name: 'project_activities', methods: ['GET', 'POST'])]
#[Security("is_granted('view', project)")] #[IsGranted('view', 'project')]
public function activitiesAction(Project $project, int $page, ActivityRepository $activityRepository) public function activitiesAction(Project $project, int $page, ActivityRepository $activityRepository)
{ {
$query = new ActivityQuery(); $query = new ActivityQuery();
@@ -319,7 +321,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/details', name: 'project_details', methods: ['GET', 'POST'])] #[Route(path: '/{id}/details', name: 'project_details', methods: ['GET', 'POST'])]
#[Security("is_granted('view', project)")] #[IsGranted('view', 'project')]
public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository, ProjectStatisticService $statisticService, CsrfTokenManagerInterface $csrfTokenManager) public function detailsAction(Project $project, TeamRepository $teamRepository, ProjectRateRepository $rateRepository, ProjectStatisticService $statisticService, CsrfTokenManagerInterface $csrfTokenManager)
{ {
$event = new ProjectMetaDefinitionEvent($project); $event = new ProjectMetaDefinitionEvent($project);
@@ -380,14 +382,14 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/rate/{rate}', name: 'admin_project_rate_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/rate/{rate}', name: 'admin_project_rate_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
public function editRateAction(Project $project, ProjectRate $rate, Request $request, ProjectRateRepository $repository): Response public function editRateAction(Project $project, ProjectRate $rate, Request $request, ProjectRateRepository $repository): Response
{ {
return $this->rateFormAction($project, $rate, $request, $repository, $this->generateUrl('admin_project_rate_edit', ['id' => $project->getId(), 'rate' => $rate->getId()])); return $this->rateFormAction($project, $rate, $request, $repository, $this->generateUrl('admin_project_rate_edit', ['id' => $project->getId(), 'rate' => $rate->getId()]));
} }
#[Route(path: '/{id}/rate', name: 'admin_project_rate_add', methods: ['GET', 'POST'])] #[Route(path: '/{id}/rate', name: 'admin_project_rate_add', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
public function addRateAction(Project $project, Request $request, ProjectRateRepository $repository): Response public function addRateAction(Project $project, Request $request, ProjectRateRepository $repository): Response
{ {
$rate = new ProjectRate(); $rate = new ProjectRate();
@@ -424,7 +426,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/edit', name: 'admin_project_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/edit', name: 'admin_project_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
public function editAction(Project $project, Request $request) public function editAction(Project $project, Request $request)
{ {
$editForm = $this->createEditForm($project); $editForm = $this->createEditForm($project);
@@ -449,7 +451,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/duplicate/{token}', name: 'admin_project_duplicate', methods: ['GET', 'POST'])] #[Route(path: '/{id}/duplicate/{token}', name: 'admin_project_duplicate', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', project)")] #[IsGranted('edit', 'project')]
public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager) public function duplicateAction(Project $project, string $token, ProjectDuplicationService $projectDuplicationService, CsrfTokenManagerInterface $csrfTokenManager)
{ {
if (!$csrfTokenManager->isTokenValid(new CsrfToken('project.duplicate', $token))) { if (!$csrfTokenManager->isTokenValid(new CsrfToken('project.duplicate', $token))) {
@@ -468,7 +470,7 @@ final class ProjectController extends AbstractController
} }
#[Route(path: '/{id}/delete', name: 'admin_project_delete', methods: ['GET', 'POST'])] #[Route(path: '/{id}/delete', name: 'admin_project_delete', methods: ['GET', 'POST'])]
#[Security("is_granted('delete', project)")] #[IsGranted('delete', 'project')]
public function deleteAction(Project $project, Request $request, ProjectStatisticService $statisticService) public function deleteAction(Project $project, Request $request, ProjectStatisticService $statisticService)
{ {
$stats = $statisticService->getProjectStatistics($project); $stats = $statisticService->getProjectStatistics($project);

View File

@@ -17,15 +17,15 @@ use App\Repository\Query\TimesheetQuery;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Timesheet\TimesheetService; use App\Timesheet\TimesheetService;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to enter times in weekly form. * Controller used to enter times in weekly form.
*/ */
#[Route(path: '/quick_entry')] #[Route(path: '/quick_entry')]
#[Security("is_granted('quick-entry')")] #[IsGranted('quick-entry')]
final class QuickEntryController extends AbstractController final class QuickEntryController extends AbstractController
{ {
public function __construct(private SystemConfiguration $configuration, private TimesheetService $timesheetService, private TimesheetRepository $repository) public function __construct(private SystemConfiguration $configuration, private TimesheetService $timesheetService, private TimesheetRepository $repository)

View File

@@ -18,13 +18,14 @@ use App\Reporting\CustomerMonthlyProjects\CustomerMonthlyProjectsRepository;
use App\Repository\Query\UserQuery; use App\Repository\Query\UserQuery;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use PhpOffice\PhpSpreadsheet\Reader\Html; use PhpOffice\PhpSpreadsheet\Reader\Html;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/reporting/customer/monthly_projects')] #[Route(path: '/reporting/customer/monthly_projects')]
#[Security("is_granted('report:customer') and is_granted('report:other')")] #[IsGranted('report:customer')]
#[IsGranted('report:other')]
final class CustomerMonthlyProjectsController extends AbstractController final class CustomerMonthlyProjectsController extends AbstractController
{ {
#[Route(path: '/view', name: 'report_customer_monthly_projects', methods: ['GET', 'POST'])] #[Route(path: '/view', name: 'report_customer_monthly_projects', methods: ['GET', 'POST'])]

View File

@@ -14,14 +14,16 @@ use App\Form\Model\DateRange;
use App\Project\ProjectStatisticService; use App\Project\ProjectStatisticService;
use App\Reporting\ProjectDateRange\ProjectDateRangeForm; use App\Reporting\ProjectDateRange\ProjectDateRangeForm;
use App\Reporting\ProjectDateRange\ProjectDateRangeQuery; use App\Reporting\ProjectDateRange\ProjectDateRangeQuery;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ProjectDateRangeController extends AbstractController final class ProjectDateRangeController extends AbstractController
{ {
#[Route(path: '/reporting/project_daterange', name: 'report_project_daterange', methods: ['GET', 'POST'])] #[Route(path: '/reporting/project_daterange', name: 'report_project_daterange', methods: ['GET', 'POST'])]
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")] #[IsGranted('report:project')]
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
public function __invoke(Request $request, ProjectStatisticService $service) public function __invoke(Request $request, ProjectStatisticService $service)
{ {
$dateFactory = $this->getDateTimeFactory(); $dateFactory = $this->getDateTimeFactory();

View File

@@ -14,14 +14,16 @@ use App\Project\ProjectStatisticService;
use App\Reporting\ProjectDetails\ProjectDetailsForm; use App\Reporting\ProjectDetails\ProjectDetailsForm;
use App\Reporting\ProjectDetails\ProjectDetailsQuery; use App\Reporting\ProjectDetails\ProjectDetailsQuery;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ProjectDetailsController extends AbstractController final class ProjectDetailsController extends AbstractController
{ {
#[Route(path: '/reporting/project_details', name: 'report_project_details', methods: ['GET'])] #[Route(path: '/reporting/project_details', name: 'report_project_details', methods: ['GET'])]
#[Security("is_granted('report:project') and is_granted('details', 'project')")] #[IsGranted('report:project')]
#[IsGranted(new Expression("is_granted('details', 'project')"))]
public function __invoke(Request $request, ProjectStatisticService $service) public function __invoke(Request $request, ProjectStatisticService $service)
{ {
$dateFactory = $this->getDateTimeFactory(); $dateFactory = $this->getDateTimeFactory();

View File

@@ -13,14 +13,16 @@ use App\Controller\AbstractController;
use App\Project\ProjectStatisticService; use App\Project\ProjectStatisticService;
use App\Reporting\ProjectInactive\ProjectInactiveForm; use App\Reporting\ProjectInactive\ProjectInactiveForm;
use App\Reporting\ProjectInactive\ProjectInactiveQuery; use App\Reporting\ProjectInactive\ProjectInactiveQuery;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ProjectInactiveController extends AbstractController final class ProjectInactiveController extends AbstractController
{ {
#[Route(path: '/reporting/project_inactive', name: 'report_project_inactive', methods: ['GET', 'POST'])] #[Route(path: '/reporting/project_inactive', name: 'report_project_inactive', methods: ['GET', 'POST'])]
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")] #[IsGranted('report:project')]
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
public function __invoke(Request $request, ProjectStatisticService $service) public function __invoke(Request $request, ProjectStatisticService $service)
{ {
$dateFactory = $this->getDateTimeFactory(); $dateFactory = $this->getDateTimeFactory();

View File

@@ -13,14 +13,16 @@ use App\Controller\AbstractController;
use App\Project\ProjectStatisticService; use App\Project\ProjectStatisticService;
use App\Reporting\ProjectView\ProjectViewForm; use App\Reporting\ProjectView\ProjectViewForm;
use App\Reporting\ProjectView\ProjectViewQuery; use App\Reporting\ProjectView\ProjectViewQuery;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ProjectViewController extends AbstractController final class ProjectViewController extends AbstractController
{ {
#[Route(path: '/reporting/project_view', name: 'report_project_view', methods: ['GET', 'POST'])] #[Route(path: '/reporting/project_view', name: 'report_project_view', methods: ['GET', 'POST'])]
#[Security("is_granted('report:project') and is_granted('budget_any', 'project')")] #[IsGranted('report:project')]
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
public function __invoke(Request $request, ProjectStatisticService $service) public function __invoke(Request $request, ProjectStatisticService $service)
{ {
$dateFactory = $this->getDateTimeFactory(); $dateFactory = $this->getDateTimeFactory();

View File

@@ -19,13 +19,13 @@ use App\Repository\Query\UserQuery;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\Timesheet\TimesheetStatisticService; use App\Timesheet\TimesheetStatisticService;
use PhpOffice\PhpSpreadsheet\Reader\Html; use PhpOffice\PhpSpreadsheet\Reader\Html;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/reporting/users')] #[Route(path: '/reporting/users')]
#[Security("is_granted('report:other')")] #[IsGranted('report:other')]
final class ReportUsersMonthController extends AbstractController final class ReportUsersMonthController extends AbstractController
{ {
#[Route(path: '/month', name: 'report_monthly_users', methods: ['GET', 'POST'])] #[Route(path: '/month', name: 'report_monthly_users', methods: ['GET', 'POST'])]

View File

@@ -19,13 +19,13 @@ use App\Repository\Query\UserQuery;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use App\Timesheet\TimesheetStatisticService; use App\Timesheet\TimesheetStatisticService;
use PhpOffice\PhpSpreadsheet\Reader\Html; use PhpOffice\PhpSpreadsheet\Reader\Html;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/reporting/users')] #[Route(path: '/reporting/users')]
#[Security("is_granted('report:other')")] #[IsGranted('report:other')]
final class ReportUsersWeekController extends AbstractController final class ReportUsersWeekController extends AbstractController
{ {
#[Route(path: '/week', name: 'report_weekly_users', methods: ['GET', 'POST'])] #[Route(path: '/week', name: 'report_weekly_users', methods: ['GET', 'POST'])]

View File

@@ -21,13 +21,13 @@ use App\Repository\UserRepository;
use App\Timesheet\TimesheetStatisticService; use App\Timesheet\TimesheetStatisticService;
use Exception; use Exception;
use PhpOffice\PhpSpreadsheet\Reader\Html; use PhpOffice\PhpSpreadsheet\Reader\Html;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/reporting/users')] #[Route(path: '/reporting/users')]
#[Security("is_granted('report:other')")] #[IsGranted('report:other')]
final class ReportUsersYearController extends AbstractController final class ReportUsersYearController extends AbstractController
{ {
/** /**

View File

@@ -13,14 +13,14 @@ use App\Model\DailyStatistic;
use App\Reporting\MonthByUser\MonthByUser; use App\Reporting\MonthByUser\MonthByUser;
use App\Reporting\MonthByUser\MonthByUserForm; use App\Reporting\MonthByUser\MonthByUserForm;
use Exception; use Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/reporting/user')] #[Route(path: '/reporting/user')]
#[Security("is_granted('report:user')")] #[IsGranted('report:user')]
final class UserMonthController extends AbstractUserReportController final class UserMonthController extends AbstractUserReportController
{ {
/** /**

View File

@@ -13,14 +13,14 @@ use App\Model\DailyStatistic;
use App\Reporting\WeekByUser\WeekByUser; use App\Reporting\WeekByUser\WeekByUser;
use App\Reporting\WeekByUser\WeekByUserForm; use App\Reporting\WeekByUser\WeekByUserForm;
use Exception; use Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/reporting/user')] #[Route(path: '/reporting/user')]
#[Security("is_granted('report:user')")] #[IsGranted('report:user')]
final class UserWeekController extends AbstractUserReportController final class UserWeekController extends AbstractUserReportController
{ {
/** /**

View File

@@ -17,14 +17,14 @@ use App\Reporting\YearByUser\YearByUser;
use App\Reporting\YearByUser\YearByUserForm; use App\Reporting\YearByUser\YearByUserForm;
use DateTime; use DateTime;
use Exception; use Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/reporting/user')] #[Route(path: '/reporting/user')]
#[Security("is_granted('report:user')")] #[IsGranted('report:user')]
final class UserYearController extends AbstractUserReportController final class UserYearController extends AbstractUserReportController
{ {
/** /**

View File

@@ -11,15 +11,15 @@ namespace App\Controller;
use App\Reporting\ReportingService; use App\Reporting\ReportingService;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to render reports. * Controller used to render reports.
*/ */
#[Route(path: '/reporting')] #[Route(path: '/reporting')]
#[Security("is_granted('view_reporting')")] #[IsGranted('view_reporting')]
final class ReportingController extends AbstractController final class ReportingController extends AbstractController
{ {
#[Route(path: '/', name: 'reporting', methods: ['GET'])] #[Route(path: '/', name: 'reporting', methods: ['GET'])]

View File

@@ -36,7 +36,6 @@ use App\Utils\PageSetup;
use App\Validator\Constraints\ColorChoices; use App\Validator\Constraints\ColorChoices;
use App\Validator\Constraints\DateTimeFormat; use App\Validator\Constraints\DateTimeFormat;
use App\Validator\Constraints\TimeFormat; use App\Validator\Constraints\TimeFormat;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\CountryType; use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\Extension\Core\Type\CurrencyType; use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
@@ -47,6 +46,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\NotNull;
@@ -57,7 +57,8 @@ use Symfony\Component\Validator\Constraints\Regex;
* Controller used for executing system relevant tasks. * Controller used for executing system relevant tasks.
*/ */
#[Route(path: '/admin/system-config')] #[Route(path: '/admin/system-config')]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('system_configuration')")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('system_configuration')]
final class SystemConfigurationController extends AbstractController final class SystemConfigurationController extends AbstractController
{ {
public function __construct(private EventDispatcherInterface $eventDispatcher, private ConfigurationRepository $repository, private SystemConfiguration $systemConfiguration, private LockdownService $lockdownService) public function __construct(private EventDispatcherInterface $eventDispatcher, private ConfigurationRepository $repository, private SystemConfiguration $systemConfiguration, private LockdownService $lockdownService)

View File

@@ -18,14 +18,14 @@ use App\Repository\Query\TagQuery;
use App\Repository\TagRepository; use App\Repository\TagRepository;
use App\Utils\DataTable; use App\Utils\DataTable;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/admin/tags')] #[Route(path: '/admin/tags')]
#[Security("is_granted('view_tag')")] #[IsGranted('view_tag')]
final class TagController extends AbstractController final class TagController extends AbstractController
{ {
/** /**
@@ -76,7 +76,7 @@ final class TagController extends AbstractController
} }
#[Route(path: '/{id}/edit', name: 'tags_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/edit', name: 'tags_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('manage_tag')")] #[IsGranted('manage_tag')]
public function editAction(Tag $tag, TagRepository $repository, Request $request) public function editAction(Tag $tag, TagRepository $repository, Request $request)
{ {
$editForm = $this->createForm(TagEditForm::class, $tag, [ $editForm = $this->createForm(TagEditForm::class, $tag, [
@@ -108,7 +108,7 @@ final class TagController extends AbstractController
} }
#[Route(path: '/create', name: 'tags_create', methods: ['GET', 'POST'])] #[Route(path: '/create', name: 'tags_create', methods: ['GET', 'POST'])]
#[Security("is_granted('manage_tag')")] #[IsGranted('manage_tag')]
public function createAction(TagRepository $repository, Request $request) public function createAction(TagRepository $repository, Request $request)
{ {
$tag = new Tag(); $tag = new Tag();
@@ -142,7 +142,7 @@ final class TagController extends AbstractController
} }
#[Route(path: '/multi-delete', name: 'tags_multi_delete', methods: ['POST'])] #[Route(path: '/multi-delete', name: 'tags_multi_delete', methods: ['POST'])]
#[Security("is_granted('delete_tag')")] #[IsGranted('delete_tag')]
public function multiDelete(TagRepository $repository, Request $request) public function multiDelete(TagRepository $repository, Request $request)
{ {
$form = $this->getMultiUpdateForm($repository); $form = $this->getMultiUpdateForm($repository);

View File

@@ -18,14 +18,14 @@ use App\Repository\Query\TeamQuery;
use App\Repository\TeamRepository; use App\Repository\TeamRepository;
use App\Utils\DataTable; use App\Utils\DataTable;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/admin/teams')] #[Route(path: '/admin/teams')]
#[Security("is_granted('view_team')")] #[IsGranted('view_team')]
final class TeamController extends AbstractController final class TeamController extends AbstractController
{ {
public function __construct(private TeamRepository $repository) public function __construct(private TeamRepository $repository)
@@ -81,14 +81,15 @@ final class TeamController extends AbstractController
* @return Response * @return Response
*/ */
#[Route(path: '/create', name: 'admin_team_create', methods: ['GET', 'POST'])] #[Route(path: '/create', name: 'admin_team_create', methods: ['GET', 'POST'])]
#[Security("is_granted('create_team')")] #[IsGranted('create_team')]
public function createTeam(Request $request): Response public function createTeam(Request $request): Response
{ {
return $this->renderEditScreen(new Team(''), $request, true); return $this->renderEditScreen(new Team(''), $request, true);
} }
#[Route(path: '/{id}/duplicate', name: 'team_duplicate', methods: ['GET', 'POST'])] #[Route(path: '/{id}/duplicate', name: 'team_duplicate', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', team) and is_granted('create_team')")] #[IsGranted('create_team')]
#[IsGranted('edit', 'team')]
public function duplicateTeam(Team $team, Request $request) public function duplicateTeam(Team $team, Request $request)
{ {
$newTeam = clone $team; $newTeam = clone $team;
@@ -103,14 +104,14 @@ final class TeamController extends AbstractController
} }
#[Route(path: '/{id}/edit', name: 'admin_team_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/edit', name: 'admin_team_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', team)")] #[IsGranted('edit', 'team')]
public function editAction(Team $team, Request $request) public function editAction(Team $team, Request $request)
{ {
return $this->renderEditScreen($team, $request); return $this->renderEditScreen($team, $request);
} }
#[Route(path: '/{id}/edit_member', name: 'admin_team_member', methods: ['GET', 'POST'])] #[Route(path: '/{id}/edit_member', name: 'admin_team_member', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', team)")] #[IsGranted('edit', 'team')]
public function editMemberAction(Team $team, Request $request) public function editMemberAction(Team $team, Request $request)
{ {
$editForm = $this->createForm(TeamEditForm::class, $team, [ $editForm = $this->createForm(TeamEditForm::class, $team, [

View File

@@ -167,14 +167,13 @@ abstract class TimesheetAbstractController extends AbstractController
protected function create(Request $request): Response protected function create(Request $request): Response
{ {
$entry = $this->service->createNewTimesheet($this->getUser()); $entry = $this->service->createNewTimesheet($this->getUser(), $request);
$preForm = $this->createFormForGetRequest(TimesheetPreCreateForm::class, $entry, [ $preForm = $this->createFormForGetRequest(TimesheetPreCreateForm::class, $entry, [
'include_user' => $this->includeUserInForms('create'), 'include_user' => $this->includeUserInForms('create'),
]); ]);
$preForm->submit($request->query->all(), false); $preForm->submit($request->query->all(), false);
$this->service->prepareNewTimesheet($entry, $request);
$createForm = $this->getCreateForm($entry); $createForm = $this->getCreateForm($entry);
$createForm->handleRequest($request); $createForm->handleRequest($request);

View File

@@ -13,19 +13,19 @@ use App\Entity\Timesheet;
use App\Event\TimesheetMetaDisplayEvent; use App\Event\TimesheetMetaDisplayEvent;
use App\Export\ServiceExport; use App\Export\ServiceExport;
use App\Form\TimesheetEditForm; use App\Form\TimesheetEditForm;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/timesheet')] #[Route(path: '/timesheet')]
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
final class TimesheetController extends TimesheetAbstractController final class TimesheetController extends TimesheetAbstractController
{ {
#[Route(path: '/', defaults: ['page' => 1], name: 'timesheet', methods: ['GET'])] #[Route(path: '/', defaults: ['page' => 1], name: 'timesheet', methods: ['GET'])]
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'timesheet_paginated', methods: ['GET'])] #[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'timesheet_paginated', methods: ['GET'])]
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
public function indexAction(int $page, Request $request): Response public function indexAction(int $page, Request $request): Response
{ {
$query = $this->createDefaultQuery(); $query = $this->createDefaultQuery();
@@ -35,42 +35,42 @@ final class TimesheetController extends TimesheetAbstractController
} }
#[Route(path: '/export/', name: 'timesheet_export', methods: ['GET', 'POST'])] #[Route(path: '/export/', name: 'timesheet_export', methods: ['GET', 'POST'])]
#[Security("is_granted('export_own_timesheet')")] #[IsGranted('export_own_timesheet')]
public function exportAction(Request $request, ServiceExport $serviceExport): Response public function exportAction(Request $request, ServiceExport $serviceExport): Response
{ {
return $this->export($request, $serviceExport); return $this->export($request, $serviceExport);
} }
#[Route(path: '/{id}/edit', name: 'timesheet_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/edit', name: 'timesheet_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', entry)")] #[IsGranted('edit', 'entry')]
public function editAction(Timesheet $entry, Request $request): Response public function editAction(Timesheet $entry, Request $request): Response
{ {
return $this->edit($entry, $request); return $this->edit($entry, $request);
} }
#[Route(path: '/{id}/duplicate', name: 'timesheet_duplicate', methods: ['GET', 'POST'])] #[Route(path: '/{id}/duplicate', name: 'timesheet_duplicate', methods: ['GET', 'POST'])]
#[Security("is_granted('duplicate', entry)")] #[IsGranted('duplicate', 'entry')]
public function duplicateAction(Timesheet $entry, Request $request): Response public function duplicateAction(Timesheet $entry, Request $request): Response
{ {
return $this->duplicate($entry, $request); return $this->duplicate($entry, $request);
} }
#[Route(path: '/multi-update', name: 'timesheet_multi_update', methods: ['POST'])] #[Route(path: '/multi-update', name: 'timesheet_multi_update', methods: ['POST'])]
#[Security("is_granted('edit_own_timesheet')")] #[IsGranted('edit_own_timesheet')]
public function multiUpdateAction(Request $request): Response public function multiUpdateAction(Request $request): Response
{ {
return $this->multiUpdate($request); return $this->multiUpdate($request);
} }
#[Route(path: '/multi-delete', name: 'timesheet_multi_delete', methods: ['POST'])] #[Route(path: '/multi-delete', name: 'timesheet_multi_delete', methods: ['POST'])]
#[Security("is_granted('delete_own_timesheet')")] #[IsGranted('delete_own_timesheet')]
public function multiDeleteAction(Request $request): Response public function multiDeleteAction(Request $request): Response
{ {
return $this->multiDelete($request); return $this->multiDelete($request);
} }
#[Route(path: '/create', name: 'timesheet_create', methods: ['GET', 'POST'])] #[Route(path: '/create', name: 'timesheet_create', methods: ['GET', 'POST'])]
#[Security("is_granted('create_own_timesheet')")] #[IsGranted('create_own_timesheet')]
public function createAction(Request $request): Response public function createAction(Request $request): Response
{ {
return $this->create($request); return $this->create($request);

View File

@@ -21,19 +21,19 @@ use App\Form\TimesheetMultiUserEditForm;
use App\Repository\Query\TimesheetQuery; use App\Repository\Query\TimesheetQuery;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/team/timesheet')] #[Route(path: '/team/timesheet')]
#[Security("is_granted('view_other_timesheet')")] #[IsGranted('view_other_timesheet')]
final class TimesheetTeamController extends TimesheetAbstractController final class TimesheetTeamController extends TimesheetAbstractController
{ {
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_timesheet', methods: ['GET'])] #[Route(path: '/', defaults: ['page' => 1], name: 'admin_timesheet', methods: ['GET'])]
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_timesheet_paginated', methods: ['GET'])] #[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_timesheet_paginated', methods: ['GET'])]
#[Security("is_granted('view_other_timesheet')")] #[IsGranted('view_other_timesheet')]
public function indexAction(int $page, Request $request): Response public function indexAction(int $page, Request $request): Response
{ {
$query = $this->createDefaultQuery(); $query = $this->createDefaultQuery();
@@ -43,35 +43,35 @@ final class TimesheetTeamController extends TimesheetAbstractController
} }
#[Route(path: '/export/', name: 'admin_timesheet_export', methods: ['GET', 'POST'])] #[Route(path: '/export/', name: 'admin_timesheet_export', methods: ['GET', 'POST'])]
#[Security("is_granted('export_other_timesheet')")] #[IsGranted('export_other_timesheet')]
public function exportAction(Request $request, ServiceExport $serviceExport): Response public function exportAction(Request $request, ServiceExport $serviceExport): Response
{ {
return $this->export($request, $serviceExport); return $this->export($request, $serviceExport);
} }
#[Route(path: '/{id}/edit', name: 'admin_timesheet_edit', methods: ['GET', 'POST'])] #[Route(path: '/{id}/edit', name: 'admin_timesheet_edit', methods: ['GET', 'POST'])]
#[Security("is_granted('edit', entry)")] #[IsGranted('edit', 'entry')]
public function editAction(Timesheet $entry, Request $request): Response public function editAction(Timesheet $entry, Request $request): Response
{ {
return $this->edit($entry, $request); return $this->edit($entry, $request);
} }
#[Route(path: '/{id}/duplicate', name: 'admin_timesheet_duplicate', methods: ['GET', 'POST'])] #[Route(path: '/{id}/duplicate', name: 'admin_timesheet_duplicate', methods: ['GET', 'POST'])]
#[Security("is_granted('duplicate', entry)")] #[IsGranted('duplicate', 'entry')]
public function duplicateAction(Timesheet $entry, Request $request): Response public function duplicateAction(Timesheet $entry, Request $request): Response
{ {
return $this->duplicate($entry, $request); return $this->duplicate($entry, $request);
} }
#[Route(path: '/create', name: 'admin_timesheet_create', methods: ['GET', 'POST'])] #[Route(path: '/create', name: 'admin_timesheet_create', methods: ['GET', 'POST'])]
#[Security("is_granted('create_other_timesheet')")] #[IsGranted('create_other_timesheet')]
public function createAction(Request $request): Response public function createAction(Request $request): Response
{ {
return $this->create($request); return $this->create($request);
} }
#[Route(path: '/create_mu', name: 'admin_timesheet_create_multiuser', methods: ['GET', 'POST'])] #[Route(path: '/create_mu', name: 'admin_timesheet_create_multiuser', methods: ['GET', 'POST'])]
#[Security("is_granted('create_other_timesheet')")] #[IsGranted('create_other_timesheet')]
public function createForMultiUserAction(Request $request): Response public function createForMultiUserAction(Request $request): Response
{ {
$entry = new MultiUserTimesheet(); $entry = new MultiUserTimesheet();
@@ -150,14 +150,14 @@ final class TimesheetTeamController extends TimesheetAbstractController
} }
#[Route(path: '/multi-update', name: 'admin_timesheet_multi_update', methods: ['POST'])] #[Route(path: '/multi-update', name: 'admin_timesheet_multi_update', methods: ['POST'])]
#[Security("is_granted('edit_other_timesheet')")] #[IsGranted('edit_other_timesheet')]
public function multiUpdateAction(Request $request): Response public function multiUpdateAction(Request $request): Response
{ {
return $this->multiUpdate($request); return $this->multiUpdate($request);
} }
#[Route(path: '/multi-delete', name: 'admin_timesheet_multi_delete', methods: ['POST'])] #[Route(path: '/multi-delete', name: 'admin_timesheet_multi_delete', methods: ['POST'])]
#[Security("is_granted('delete_other_timesheet')")] #[IsGranted('delete_other_timesheet')]
public function multiDeleteAction(Request $request): Response public function multiDeleteAction(Request $request): Response
{ {
return $this->multiDelete($request); return $this->multiDelete($request);

View File

@@ -25,19 +25,20 @@ use App\Repository\UserRepository;
use App\User\UserService; use App\User\UserService;
use App\Utils\DataTable; use App\Utils\DataTable;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller used to manage users in the admin part of the site. * Controller used to manage users in the admin part of the site.
*/ */
#[Route(path: '/admin/user')] #[Route(path: '/admin/user')]
#[Security("is_granted('IS_AUTHENTICATED_FULLY') and is_granted('view_user')")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
#[IsGranted('view_user')]
final class UserController extends AbstractController final class UserController extends AbstractController
{ {
public function __construct(private UserPasswordHasherInterface $passwordHasher, private UserRepository $repository, private EventDispatcherInterface $dispatcher) public function __construct(private UserPasswordHasherInterface $passwordHasher, private UserRepository $repository, private EventDispatcherInterface $dispatcher)
@@ -110,7 +111,7 @@ final class UserController extends AbstractController
} }
#[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])] #[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])]
#[Security("is_granted('create_user')")] #[IsGranted('create_user')]
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository): Response public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository): Response
{ {
$user = $this->createNewDefaultUser($config); $user = $this->createNewDefaultUser($config);
@@ -139,7 +140,7 @@ final class UserController extends AbstractController
} }
#[Route(path: '/{id}/delete', name: 'admin_user_delete', methods: ['GET', 'POST'])] #[Route(path: '/{id}/delete', name: 'admin_user_delete', methods: ['GET', 'POST'])]
#[Security("is_granted('delete', userToDelete)")] #[IsGranted('delete', 'userToDelete')]
public function deleteAction(User $userToDelete, Request $request, TimesheetRepository $repository, UserService $userService): Response public function deleteAction(User $userToDelete, Request $request, TimesheetRepository $repository, UserService $userService): Response
{ {
// $userToDelete MUST not be called $user, as $user is always the current user! // $userToDelete MUST not be called $user, as $user is always the current user!
@@ -191,7 +192,7 @@ final class UserController extends AbstractController
} }
#[Route(path: '/export', name: 'user_export', methods: ['GET'])] #[Route(path: '/export', name: 'user_export', methods: ['GET'])]
#[Security("is_granted('view_user')")] #[IsGranted('view_user')]
public function exportAction(Request $request, UserExporter $exporter) public function exportAction(Request $request, UserExporter $exporter)
{ {
$query = new UserQuery(); $query = new UserQuery();

View File

@@ -9,16 +9,16 @@
namespace App\Controller; namespace App\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/widgets')] #[Route(path: '/widgets')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")] #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
final class WidgetController extends AbstractController final class WidgetController extends AbstractController
{ {
#[Route(path: '/working-time/{year}/{week}', requirements: ['year' => '[1-9]\d*', 'week' => '[0-9]\d*'], name: 'widgets_working_time_chart', methods: ['GET'])] #[Route(path: '/working-time/{year}/{week}', requirements: ['year' => '[1-9]\d*', 'week' => '[0-9]\d*'], name: 'widgets_working_time_chart', methods: ['GET'])]
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
public function workingtimechartAction($year, $week): Response public function workingtimechartAction($year, $week): Response
{ {
return $this->render('widget/paginatedworkingtimechart.html.twig', [ return $this->render('widget/paginatedworkingtimechart.html.twig', [

View File

@@ -15,18 +15,18 @@ use App\Form\Type\LanguageType;
use App\Form\Type\SkinType; use App\Form\Type\SkinType;
use App\Form\Type\TimezoneType; use App\Form\Type\TimezoneType;
use App\User\UserService; use App\User\UserService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/wizard')] #[Route(path: '/wizard')]
#[Security("is_granted('IS_AUTHENTICATED_FULLY')")] #[IsGranted('IS_AUTHENTICATED_FULLY')]
final class WizardController extends AbstractController final class WizardController extends AbstractController
{ {
#[Route(path: '/{wizard}', name: 'wizard', methods: ['GET', 'POST'])] #[Route(path: '/{wizard}', name: 'wizard', methods: ['GET', 'POST'])]
#[Security("is_granted('view_own_timesheet')")] #[IsGranted('view_own_timesheet')]
public function wizard(Request $request, UserService $userService, string $wizard): Response public function wizard(Request $request, UserService $userService, string $wizard): Response
{ {
$user = $this->getUser(); $user = $this->getUser();

View File

@@ -92,7 +92,7 @@ final class InvoiceFixtures extends Fixture
]; ];
} }
protected function generatePaymentDetails(Generator $faker): string private function generatePaymentDetails(Generator $faker): string
{ {
return return
'Acme Bank' . PHP_EOL . 'Acme Bank' . PHP_EOL .
@@ -101,7 +101,7 @@ final class InvoiceFixtures extends Fixture
; ;
} }
protected function generateContact(Generator $faker): string private function generateContact(Generator $faker): string
{ {
return return
'Phone: ' . $faker->phoneNumber() . PHP_EOL . 'Phone: ' . $faker->phoneNumber() . PHP_EOL .
@@ -110,7 +110,7 @@ final class InvoiceFixtures extends Fixture
; ;
} }
protected function generateAddress(Generator $faker): string private function generateAddress(Generator $faker): string
{ {
return return
$faker->streetAddress() . PHP_EOL . $faker->streetAddress() . PHP_EOL .

View File

@@ -40,7 +40,7 @@ final class TeamFixtures extends Fixture
* @param ObjectManager $manager * @param ObjectManager $manager
* @return array<int|string, User> * @return array<int|string, User>
*/ */
protected function getAllUsers(ObjectManager $manager): array private function getAllUsers(ObjectManager $manager): array
{ {
$all = []; $all = [];
/** @var User[] $entries */ /** @var User[] $entries */
@@ -56,7 +56,7 @@ final class TeamFixtures extends Fixture
* @param ObjectManager $manager * @param ObjectManager $manager
* @return array<int|string, Project> * @return array<int|string, Project>
*/ */
protected function getAllProjects(ObjectManager $manager): array private function getAllProjects(ObjectManager $manager): array
{ {
$all = []; $all = [];

View File

@@ -117,6 +117,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
} }
$allTags = $this->getAllTags($manager); $allTags = $this->getAllTags($manager);
/** @var array<Timesheet> $entries */
$entries = $this->findRandom($manager, Timesheet::class, min($all, self::ADD_TAGS_MAX_ENTRIES)); $entries = $this->findRandom($manager, Timesheet::class, min($all, self::ADD_TAGS_MAX_ENTRIES));
foreach ($entries as $temp) { foreach ($entries as $temp) {
$tagAmount = rand(0, self::MAX_TAG_PER_ENTRY); $tagAmount = rand(0, self::MAX_TAG_PER_ENTRY);
@@ -132,9 +133,17 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
$manager->clear(); $manager->clear();
} }
private function findRandom(ObjectManager $manager, string $class, int $amount) /**
* @template T of object
* @param ObjectManager $manager
* @param class-string<T> $class
* @param int $amount
* @return array<int, T>
*/
private function findRandom(ObjectManager $manager, string $class, int $amount): array
{ {
$qb = $manager->getRepository($class)->createQueryBuilder('entity'); $qb = $manager->getRepository($class)->createQueryBuilder('entity');
/** @var array<int> $limits */
$limits = $qb $limits = $qb
->select('MIN(entity.id)', 'MAX(entity.id)') ->select('MIN(entity.id)', 'MAX(entity.id)')
->getQuery() ->getQuery()
@@ -150,18 +159,17 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
$qb = $manager->getRepository($class)->createQueryBuilder('entity'); $qb = $manager->getRepository($class)->createQueryBuilder('entity');
return $qb /** @var array<int, T> $result */
->where($qb->expr()->in('entity.id', $ids)) $result = $qb->where($qb->expr()->in('entity.id', $ids))->setMaxResults($amount)->getQuery()->getResult();
->setMaxResults($amount)
->getQuery() return $result;
->getResult();
} }
/** /**
* @param ObjectManager $manager * @param ObjectManager $manager
* @return array<int|string, Tag> * @return array<int|string, Tag>
*/ */
protected function getAllTags(ObjectManager $manager): array private function getAllTags(ObjectManager $manager): array
{ {
return $this->findRandom($manager, Tag::class, 50); return $this->findRandom($manager, Tag::class, 50);
} }
@@ -170,7 +178,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
* @param ObjectManager $manager * @param ObjectManager $manager
* @return array<int|string, User> * @return array<int|string, User>
*/ */
protected function getAllUsers(ObjectManager $manager): array private function getAllUsers(ObjectManager $manager): array
{ {
$all = []; $all = [];
/** @var User[] $entries */ /** @var User[] $entries */
@@ -186,7 +194,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
* @param ObjectManager $manager * @param ObjectManager $manager
* @return array<int|string, Project> * @return array<int|string, Project>
*/ */
protected function getAllProjects(ObjectManager $manager): array private function getAllProjects(ObjectManager $manager): array
{ {
return $this->findRandom($manager, Project::class, 50); return $this->findRandom($manager, Project::class, 50);
} }
@@ -195,12 +203,12 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
* @param ObjectManager $manager * @param ObjectManager $manager
* @return array<int|string, Activity> * @return array<int|string, Activity>
*/ */
protected function getAllActivities(ObjectManager $manager): array private function getAllActivities(ObjectManager $manager): array
{ {
return $this->findRandom($manager, Activity::class, 50); return $this->findRandom($manager, Activity::class, 50);
} }
private function createTimesheetEntry(User $user, Activity $activity, Project $project, ?string $description, bool $setEndDate) private function createTimesheetEntry(User $user, Activity $activity, Project $project, ?string $description, bool $setEndDate): Timesheet
{ {
$start = $this->getRandomFirstDay(); $start = $this->getRandomFirstDay();
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds'); $start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');

View File

@@ -59,10 +59,8 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
/** /**
* Default users for all test cases * Default users for all test cases
*
* @param ObjectManager $manager
*/ */
private function loadDefaultAccounts(ObjectManager $manager) private function loadDefaultAccounts(ObjectManager $manager): void
{ {
$allUsers = $this->getUserDefinition(); $allUsers = $this->getUserDefinition();
foreach ($allUsers as $userData) { foreach ($allUsers as $userData) {
@@ -74,8 +72,8 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
$user->setRoles([$userData[4]]); $user->setRoles([$userData[4]]);
$user->setAvatar($userData[5]); $user->setAvatar($userData[5]);
$user->setEnabled($userData[6]); $user->setEnabled($userData[6]);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD)); $user->setPassword($this->passwordHasher->hashPassword($user, $userData[8]));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN)); $user->setApiToken($this->passwordHasher->hashPassword($user, $userData[9]));
$manager->persist($user); $manager->persist($user);
$prefs = $this->getUserPreferences($user, $userData[7]); $prefs = $this->getUserPreferences($user, $userData[7]);
@@ -117,11 +115,9 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
} }
/** /**
* Generate randomized test users, which don't have API access. * Generate randomized test users, without API access.
*
* @param ObjectManager $manager
*/ */
private function loadTestUsers(ObjectManager $manager) private function loadTestUsers(ObjectManager $manager): void
{ {
$faker = Factory::create(); $faker = Factory::create();
$existingName = []; $existingName = [];
@@ -161,10 +157,7 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
$manager->clear(); $manager->clear();
} }
/** private function getUserDefinition(): array
* @return array
*/
protected function getUserDefinition(): array
{ {
// alias = $userData[0] // alias = $userData[0]
// title = $userData[1] // title = $userData[1]
@@ -174,6 +167,8 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
// avatar = $userData[5] // avatar = $userData[5]
// enabled = $userData[6] // enabled = $userData[6]
// timezone = $userData[7] // timezone = $userData[7]
// password = $userData[8]
// api = $userData[9]
return [ return [
[ [
@@ -185,6 +180,20 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
self::DEFAULT_AVATAR, self::DEFAULT_AVATAR,
true, true,
'America/Vancouver', 'America/Vancouver',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
],
[
'John Doe',
'Developer',
'user',
'user@example.com',
User::ROLE_USER,
self::DEFAULT_AVATAR,
true,
'America/Vancouver',
'password',
'password',
], ],
// inactive user to test login // inactive user to test login
[ [
@@ -196,6 +205,8 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
self::DEFAULT_AVATAR, self::DEFAULT_AVATAR,
false, false,
'Australia/Sydney', 'Australia/Sydney',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
], ],
[ [
'Tony Maier', 'Tony Maier',
@@ -206,6 +217,20 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg', 'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg',
true, true,
'Asia/Bangkok', 'Asia/Bangkok',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
],
[
'Tony Maier',
'Head of Sales',
'teamlead',
'teamlead@example.com',
User::ROLE_TEAMLEAD,
'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg',
true,
'Asia/Bangkok',
'password',
'password',
], ],
// no avatar to test default image macro // no avatar to test default image macro
[ [
@@ -217,6 +242,20 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
null, null,
true, true,
'Europe/London', 'Europe/London',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
],
[
'Anna Smith',
'Administrator',
'administrator',
'administrator@example.com',
User::ROLE_ADMIN,
null,
true,
'Europe/London',
'password',
'password',
], ],
// no alias to test twig username macro // no alias to test twig username macro
[ [
@@ -228,7 +267,21 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
'/touch-icon-192x192.png', '/touch-icon-192x192.png',
true, true,
'Europe/Berlin', 'Europe/Berlin',
] self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
],
[
null,
'Super Administrator',
'super_admin',
'super_admin@example.com',
User::ROLE_SUPER_ADMIN,
'/touch-icon-192x192.png',
true,
'Europe/Berlin',
'password',
'password',
],
]; ];
} }
} }

View File

@@ -107,7 +107,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
#[Serializer\Expose] #[Serializer\Expose]
#[Serializer\Groups(['Customer'])] #[Serializer\Groups(['Customer'])]
#[Exporter\Expose(label: 'currency')] #[Exporter\Expose(label: 'currency')]
private string $currency = self::DEFAULT_CURRENCY; private ?string $currency = self::DEFAULT_CURRENCY;
#[ORM\Column(name: 'phone', type: 'string', length: 30, nullable: true)] #[ORM\Column(name: 'phone', type: 'string', length: 30, nullable: true)]
#[Assert\Length(max: 30)] #[Assert\Length(max: 30)]
#[Serializer\Expose] #[Serializer\Expose]
@@ -197,11 +197,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->id; return $this->id;
} }
public function setName(?string $name): Customer public function setName(?string $name): void
{ {
$this->name = $name; $this->name = $name;
return $this;
} }
public function getName(): ?string public function getName(): ?string
@@ -209,11 +207,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->name; return $this->name;
} }
public function setNumber(?string $number): Customer public function setNumber(?string $number): void
{ {
$this->number = $number; $this->number = $number;
return $this;
} }
public function getNumber(): ?string public function getNumber(): ?string
@@ -221,11 +217,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->number; return $this->number;
} }
public function setComment(?string $comment): Customer public function setComment(?string $comment): void
{ {
$this->comment = $comment; $this->comment = $comment;
return $this;
} }
public function getComment(): ?string public function getComment(): ?string
@@ -233,11 +227,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->comment; return $this->comment;
} }
public function setVisible(bool $visible): Customer public function setVisible(bool $visible): void
{ {
$this->visible = $visible; $this->visible = $visible;
return $this;
} }
public function isVisible(): bool public function isVisible(): bool
@@ -255,11 +247,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->billable; return $this->billable;
} }
public function setCompany(?string $company): Customer public function setCompany(?string $company): void
{ {
$this->company = $company; $this->company = $company;
return $this;
} }
public function getCompany(): ?string public function getCompany(): ?string
@@ -272,18 +262,14 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->vatId; return $this->vatId;
} }
public function setVatId(?string $vatId): Customer public function setVatId(?string $vatId): void
{ {
$this->vatId = $vatId; $this->vatId = $vatId;
return $this;
} }
public function setContact(?string $contact): Customer public function setContact(?string $contact): void
{ {
$this->contact = $contact; $this->contact = $contact;
return $this;
} }
public function getContact(): ?string public function getContact(): ?string
@@ -291,11 +277,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->contact; return $this->contact;
} }
public function setAddress(?string $address): Customer public function setAddress(?string $address): void
{ {
$this->address = $address; $this->address = $address;
return $this;
} }
public function getAddress(): ?string public function getAddress(): ?string
@@ -303,11 +287,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->address; return $this->address;
} }
public function setCountry(?string $country): Customer public function setCountry(?string $country): void
{ {
$this->country = $country; $this->country = $country;
return $this;
} }
public function getCountry(): ?string public function getCountry(): ?string
@@ -315,23 +297,19 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->country; return $this->country;
} }
public function setCurrency(string $currency): Customer public function setCurrency(?string $currency): void
{ {
$this->currency = $currency; $this->currency = $currency;
return $this;
} }
public function getCurrency(): string public function getCurrency(): ?string
{ {
return $this->currency; return $this->currency;
} }
public function setPhone(?string $phone): Customer public function setPhone(?string $phone): void
{ {
$this->phone = $phone; $this->phone = $phone;
return $this;
} }
public function getPhone(): ?string public function getPhone(): ?string
@@ -339,11 +317,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->phone; return $this->phone;
} }
public function setFax(?string $fax): Customer public function setFax(?string $fax): void
{ {
$this->fax = $fax; $this->fax = $fax;
return $this;
} }
public function getFax(): ?string public function getFax(): ?string
@@ -351,11 +327,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->fax; return $this->fax;
} }
public function setMobile(?string $mobile): Customer public function setMobile(?string $mobile): void
{ {
$this->mobile = $mobile; $this->mobile = $mobile;
return $this;
} }
public function getMobile(): ?string public function getMobile(): ?string
@@ -363,11 +337,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->mobile; return $this->mobile;
} }
public function setEmail(?string $mail): Customer public function setEmail(?string $mail): void
{ {
$this->email = $mail; $this->email = $mail;
return $this;
} }
public function getEmail(): ?string public function getEmail(): ?string
@@ -375,11 +347,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->email; return $this->email;
} }
public function setHomepage(?string $homepage): Customer public function setHomepage(?string $homepage): void
{ {
$this->homepage = $homepage; $this->homepage = $homepage;
return $this;
} }
public function getHomepage(): ?string public function getHomepage(): ?string
@@ -387,11 +357,9 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
return $this->homepage; return $this->homepage;
} }
public function setTimezone(string $timezone): Customer public function setTimezone(?string $timezone): void
{ {
$this->timezone = $timezone; $this->timezone = $timezone;
return $this;
} }
public function getTimezone(): ?string public function getTimezone(): ?string

View File

@@ -90,6 +90,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
#[Assert\NotNull] #[Assert\NotNull]
private ?DateTime $date = null; private ?DateTime $date = null;
/** /**
* Time records start date-time.
*
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC. * Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
*/ */
#[ORM\Column(name: 'start_time', type: 'datetime', nullable: false)] #[ORM\Column(name: 'start_time', type: 'datetime', nullable: false)]
@@ -100,6 +102,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
#[Serializer\Accessor(getter: 'getBegin')] #[Serializer\Accessor(getter: 'getBegin')]
private ?DateTime $begin = null; private ?DateTime $begin = null;
/** /**
* Time records end date-time.
*
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC. * Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
*/ */
#[ORM\Column(name: 'end_time', type: 'datetime', nullable: true)] #[ORM\Column(name: 'end_time', type: 'datetime', nullable: true)]

View File

@@ -165,7 +165,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
#[ORM\Column(name: 'email', type: 'string', length: 180, nullable: false)] #[ORM\Column(name: 'email', type: 'string', length: 180, nullable: false)]
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Length(min: 2, max: 180)] #[Assert\Length(min: 2, max: 180)]
#[Assert\Email(groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Email(mode: 'html5', groups: ['Registration', 'UserCreate', 'Profile'])]
private ?string $email = null; private ?string $email = null;
#[ORM\Column(name: 'account', type: 'string', length: 30, nullable: true)] #[ORM\Column(name: 'account', type: 'string', length: 30, nullable: true)]
#[Assert\Length(max: 30, groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Length(max: 30, groups: ['Registration', 'UserCreate', 'Profile'])]

View File

@@ -14,9 +14,9 @@ use App\Event\ConfigureMainMenuEvent;
use App\Utils\MenuItemModel; use App\Utils\MenuItemModel;
use KevinPapst\TablerBundle\Event\MenuEvent; use KevinPapst\TablerBundle\Event\MenuEvent;
use KevinPapst\TablerBundle\Model\MenuItemInterface; use KevinPapst\TablerBundle\Model\MenuItemInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
/** /**
* Class MenuBuilder configures the main navigation. * Class MenuBuilder configures the main navigation.

View File

@@ -12,8 +12,8 @@ namespace App\EventSubscriber;
use App\Entity\User; use App\Entity\User;
use App\Event\ConfigureMainMenuEvent; use App\Event\ConfigureMainMenuEvent;
use App\Utils\MenuItemModel; use App\Utils\MenuItemModel;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
final class MenuFavoritesSubscriber implements EventSubscriberInterface final class MenuFavoritesSubscriber implements EventSubscriberInterface
{ {

View File

@@ -12,9 +12,9 @@ namespace App\EventSubscriber;
use App\Entity\User; use App\Entity\User;
use App\Utils\MenuItemModel; use App\Utils\MenuItemModel;
use KevinPapst\TablerBundle\Event\UserDetailsEvent; use KevinPapst\TablerBundle\Event\UserDetailsEvent;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;
/** /**
* @internal * @internal

View File

@@ -29,11 +29,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**

View File

@@ -10,10 +10,10 @@
namespace App\Form\Extension; namespace App\Form\Extension;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
final class UserExtension extends AbstractTypeExtension final class UserExtension extends AbstractTypeExtension
{ {

View File

@@ -10,6 +10,7 @@
namespace App\Form; namespace App\Form;
use App\Form\Type\DescriptionType; use App\Form\Type\DescriptionType;
use App\Form\Type\MetaFieldsCollectionType;
use App\Form\Type\TagsInputType; use App\Form\Type\TagsInputType;
use App\Form\Type\UserType; use App\Form\Type\UserType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@@ -29,6 +30,7 @@ final class TimesheetPreCreateForm extends AbstractType
$this->addActivity($builder, null, null, ['required' => false]); $this->addActivity($builder, null, null, ['required' => false]);
$builder->add('description', DescriptionType::class, ['required' => false]); $builder->add('description', DescriptionType::class, ['required' => false]);
$builder->add('tags', TagsInputType::class, ['required' => false]); $builder->add('tags', TagsInputType::class, ['required' => false]);
$builder->add('metaFields', MetaFieldsCollectionType::class);
if ($options['include_user']) { if ($options['include_user']) {
$builder->add('user', UserType::class, ['required' => false]); $builder->add('user', UserType::class, ['required' => false]);
} }

View File

@@ -10,13 +10,13 @@
namespace App\Form\Type; namespace App\Form\Type;
use App\Entity\Timesheet; use App\Entity\Timesheet;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
final class QuickEntryTimesheetType extends AbstractType final class QuickEntryTimesheetType extends AbstractType
{ {

View File

@@ -229,12 +229,11 @@ final class InvoiceModel
public function getCurrency(): string public function getCurrency(): string
{ {
if (null === $this->getCustomer()) { if (null !== $this->getCustomer() && $this->getCustomer()->getCurrency() !== null) {
// this should be set from the configuration return $this->getCustomer()->getCurrency();
return Customer::DEFAULT_CURRENCY;
} }
return $this->getCustomer()->getCurrency(); return Customer::DEFAULT_CURRENCY;
} }
public function toArray(): array public function toArray(): array

View File

@@ -62,7 +62,7 @@ final class FavoriteRecordService
$bookmark = new Bookmark(); $bookmark = new Bookmark();
$bookmark->setUser($user); $bookmark->setUser($user);
$bookmark->setType('favorite'); $bookmark->setType('favorite');
$bookmark->setName('timesheet'); $bookmark->setName('recent');
} }
return $bookmark; return $bookmark;

View File

@@ -17,7 +17,7 @@ use App\Utils\FormFormatConverter;
use App\Utils\JavascriptFormatConverter; use App\Utils\JavascriptFormatConverter;
use App\Utils\LocaleFormatter; use App\Utils\LocaleFormatter;
use DateTime; use DateTime;
use Symfony\Component\Security\Core\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\LocaleAwareInterface;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;

View File

@@ -13,7 +13,7 @@ use App\Entity\User;
use App\Widget\WidgetException; use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
use App\Widget\WidgetService; use App\Widget\WidgetService;
use Symfony\Component\Security\Core\Security; use Symfony\Bundle\SecurityBundle\Security;
use Twig\Environment; use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface; use Twig\Extension\RuntimeExtensionInterface;

View File

@@ -11,6 +11,7 @@ namespace App\Utils;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
final class ProfileManager final class ProfileManager
{ {
@@ -52,7 +53,7 @@ final class ProfileManager
return $profile; return $profile;
} }
public function setProfile(Session $session, string $profile): void public function setProfile(SessionInterface $session, string $profile): void
{ {
if ($profile === self::PROFILE_MOBILE) { if ($profile === self::PROFILE_MOBILE) {
$session->set(self::SESSION_PROFILE, $profile); $session->set(self::SESSION_PROFILE, $profile);

View File

@@ -16,7 +16,7 @@ final class ColorChoices extends Constraint
public const COLOR_CHOICES_ERROR = 'ui5hffg-dsfef3-1234-5678-2g8jkfr56d84'; public const COLOR_CHOICES_ERROR = 'ui5hffg-dsfef3-1234-5678-2g8jkfr56d84';
public const COLOR_CHOICES_NAME_ERROR = 'ui5hffg-dsfef3-1234-5679-2g8jkfr56d84'; public const COLOR_CHOICES_NAME_ERROR = 'ui5hffg-dsfef3-1234-5679-2g8jkfr56d84';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::COLOR_CHOICES_ERROR => 'COLOR_CHOICES_ERROR', self::COLOR_CHOICES_ERROR => 'COLOR_CHOICES_ERROR',
self::COLOR_CHOICES_NAME_ERROR => 'COLOR_CHOICES_NAME_ERROR', self::COLOR_CHOICES_NAME_ERROR => 'COLOR_CHOICES_NAME_ERROR',
]; ];

View File

@@ -15,7 +15,7 @@ final class DateTimeFormat extends Constraint
{ {
public const INVALID_FORMAT = 'kimai-datetime-00'; public const INVALID_FORMAT = 'kimai-datetime-00';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::INVALID_FORMAT => 'The given value is not a valid datetime format.', self::INVALID_FORMAT => 'The given value is not a valid datetime format.',
]; ];

View File

@@ -16,7 +16,7 @@ final class HexColor extends Constraint
{ {
public const HEX_COLOR_ERROR = 'xd5hffg-dsfef3-426a-83d7-2g8jkfr56d84'; public const HEX_COLOR_ERROR = 'xd5hffg-dsfef3-426a-83d7-2g8jkfr56d84';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::HEX_COLOR_ERROR => 'HEX_COLOR_ERROR', self::HEX_COLOR_ERROR => 'HEX_COLOR_ERROR',
]; ];

View File

@@ -16,7 +16,7 @@ final class Project extends Constraint
{ {
public const END_BEFORE_BEGIN_ERROR = 'kimai-project-00'; public const END_BEFORE_BEGIN_ERROR = 'kimai-project-00';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::END_BEFORE_BEGIN_ERROR => 'End date must not be earlier then start date.', self::END_BEFORE_BEGIN_ERROR => 'End date must not be earlier then start date.',
]; ];

View File

@@ -16,7 +16,7 @@ final class Role extends Constraint
{ {
public const ROLE_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d84'; public const ROLE_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d84';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::ROLE_ERROR => 'ROLE_ERROR', self::ROLE_ERROR => 'ROLE_ERROR',
]; ];

View File

@@ -16,7 +16,7 @@ final class RoleName extends Constraint
{ {
public const ROLE_NAME_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d85'; public const ROLE_NAME_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d85';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::ROLE_NAME_ERROR => 'ROLE_NAME_ERROR', self::ROLE_NAME_ERROR => 'ROLE_NAME_ERROR',
]; ];

View File

@@ -16,7 +16,7 @@ final class Team extends Constraint
{ {
public const MISSING_TEAMLEAD = 'kimai-team-001'; public const MISSING_TEAMLEAD = 'kimai-team-001';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::MISSING_TEAMLEAD => 'At least one team leader must be assigned to the team.', self::MISSING_TEAMLEAD => 'At least one team leader must be assigned to the team.',
]; ];

View File

@@ -15,7 +15,7 @@ final class TimeFormat extends Constraint
{ {
public const INVALID_FORMAT = 'kimai-time-00'; public const INVALID_FORMAT = 'kimai-time-00';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::INVALID_FORMAT => 'The given value is not a valid time.', self::INVALID_FORMAT => 'The given value is not a valid time.',
]; ];

View File

@@ -23,7 +23,7 @@ final class TimesheetBasic extends TimesheetConstraint
public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92'; public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92';
public const PROJECT_DISALLOWS_GLOBAL_ACTIVITY = 'kimai-timesheet-93'; public const PROJECT_DISALLOWS_GLOBAL_ACTIVITY = 'kimai-timesheet-93';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.', self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
self::END_BEFORE_BEGIN_ERROR => 'End date must not be earlier then start date.', self::END_BEFORE_BEGIN_ERROR => 'End date must not be earlier then start date.',
self::MISSING_ACTIVITY_ERROR => 'An activity needs to be selected.', self::MISSING_ACTIVITY_ERROR => 'An activity needs to be selected.',

View File

@@ -13,7 +13,7 @@ final class TimesheetExported extends TimesheetConstraint
{ {
public const TIMESHEET_EXPORTED = 'kimai-timesheet-exported-01'; public const TIMESHEET_EXPORTED = 'kimai-timesheet-exported-01';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::TIMESHEET_EXPORTED => 'This timesheet is already exported.', self::TIMESHEET_EXPORTED => 'This timesheet is already exported.',
]; ];

View File

@@ -10,7 +10,7 @@
namespace App\Validator\Constraints; namespace App\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity; use App\Entity\Timesheet as TimesheetEntity;
use Symfony\Component\Security\Core\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;

View File

@@ -13,7 +13,7 @@ final class TimesheetFutureTimes extends TimesheetConstraint
{ {
public const BEGIN_IN_FUTURE_ERROR = 'kimai-timesheet-future-times-01'; public const BEGIN_IN_FUTURE_ERROR = 'kimai-timesheet-future-times-01';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::BEGIN_IN_FUTURE_ERROR => 'The begin date cannot be in the future.', self::BEGIN_IN_FUTURE_ERROR => 'The begin date cannot be in the future.',
]; ];

View File

@@ -13,7 +13,7 @@ final class TimesheetLockdown extends TimesheetConstraint
{ {
public const PERIOD_LOCKED = 'kimai-timesheet-lockdown-01'; public const PERIOD_LOCKED = 'kimai-timesheet-lockdown-01';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::PERIOD_LOCKED => 'This period is locked, please choose a later date.', self::PERIOD_LOCKED => 'This period is locked, please choose a later date.',
]; ];

View File

@@ -11,7 +11,7 @@ namespace App\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity; use App\Entity\Timesheet as TimesheetEntity;
use App\Timesheet\LockdownService; use App\Timesheet\LockdownService;
use Symfony\Component\Security\Core\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;

View File

@@ -14,7 +14,7 @@ final class TimesheetLongRunning extends TimesheetConstraint
public const LONG_RUNNING = 'kimai-timesheet-long-running-01'; public const LONG_RUNNING = 'kimai-timesheet-long-running-01';
public const MAXIMUM = 'kimai-timesheet-long-running-02'; public const MAXIMUM = 'kimai-timesheet-long-running-02';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::LONG_RUNNING => 'TIMESHEET_LONG_RUNNING', self::LONG_RUNNING => 'TIMESHEET_LONG_RUNNING',
self::MAXIMUM => 'MAXIMUM', self::MAXIMUM => 'MAXIMUM',
]; ];

View File

@@ -22,7 +22,7 @@ final class TimesheetMultiUpdate extends Constraint
public const DISABLED_CUSTOMER_ERROR = 'ts-multi-update-89'; public const DISABLED_CUSTOMER_ERROR = 'ts-multi-update-89';
public const HOURLY_RATE_FIXED_RATE = 'ts-multi-update-90'; public const HOURLY_RATE_FIXED_RATE = 'ts-multi-update-90';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::MISSING_ACTIVITY_ERROR => 'You need to choose an activity, if the project should be changed.', self::MISSING_ACTIVITY_ERROR => 'You need to choose an activity, if the project should be changed.',
self::MISSING_PROJECT_ERROR => 'A project needs to be selected.', self::MISSING_PROJECT_ERROR => 'A project needs to be selected.',
self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch: chosen project does not match the activity project.', self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch: chosen project does not match the activity project.',

View File

@@ -16,7 +16,7 @@ final class TimesheetMultiUser extends Constraint
{ {
public const MISSING_USER_OR_TEAM = 'ts-multi-user-01'; public const MISSING_USER_OR_TEAM = 'ts-multi-user-01';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::MISSING_USER_OR_TEAM => 'You must select at least one user or team.', self::MISSING_USER_OR_TEAM => 'You must select at least one user or team.',
]; ];

View File

@@ -13,7 +13,7 @@ final class TimesheetOverlapping extends TimesheetConstraint
{ {
public const RECORD_OVERLAPPING = 'kimai-timesheet-overlapping-01'; public const RECORD_OVERLAPPING = 'kimai-timesheet-overlapping-01';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::RECORD_OVERLAPPING => 'You already have an entry for this time.', self::RECORD_OVERLAPPING => 'You already have an entry for this time.',
]; ];

View File

@@ -13,7 +13,7 @@ final class TimesheetRestart extends TimesheetConstraint
{ {
public const START_DISALLOWED = 'kimai-timesheet-restart-01'; public const START_DISALLOWED = 'kimai-timesheet-restart-01';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::START_DISALLOWED => 'You are not allowed to start this timesheet record.', self::START_DISALLOWED => 'You are not allowed to start this timesheet record.',
]; ];

View File

@@ -11,7 +11,7 @@ namespace App\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity; use App\Entity\Timesheet as TimesheetEntity;
use App\Timesheet\TrackingModeService; use App\Timesheet\TrackingModeService;
use Symfony\Component\Security\Core\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;

View File

@@ -13,7 +13,7 @@ final class TimesheetZeroDuration extends TimesheetConstraint
{ {
public const ZERO_DURATION_ERROR = 'kimai-timesheet-zero-duration-01'; public const ZERO_DURATION_ERROR = 'kimai-timesheet-zero-duration-01';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::ZERO_DURATION_ERROR => 'Duration cannot be zero.', self::ZERO_DURATION_ERROR => 'Duration cannot be zero.',
]; ];

View File

@@ -19,7 +19,7 @@ final class User extends Constraint
public const USER_EXISTING_EMAIL_AS_NAME = 'kimai-user-02'; public const USER_EXISTING_EMAIL_AS_NAME = 'kimai-user-02';
public const USER_EXISTING_NAME_AS_EMAIL = 'kimai-user-03'; public const USER_EXISTING_NAME_AS_EMAIL = 'kimai-user-03';
protected static $errorNames = [ protected const ERROR_NAMES = [
self::USER_EXISTING_EMAIL => 'The email is already used.', self::USER_EXISTING_EMAIL => 'The email is already used.',
self::USER_EXISTING_NAME => 'The username is already used.', self::USER_EXISTING_NAME => 'The username is already used.',
self::USER_EXISTING_EMAIL_AS_NAME => 'An equal username is already used.', self::USER_EXISTING_EMAIL_AS_NAME => 'An equal username is already used.',

View File

@@ -379,15 +379,6 @@
"sebastian/version": { "sebastian/version": {
"version": "2.0.1" "version": "2.0.1"
}, },
"sensio/framework-extra-bundle": {
"version": "4.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "4.0",
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
}
},
"setasign/fpdi": { "setasign/fpdi": {
"version": "1.6.2" "version": "1.6.2"
}, },

View File

@@ -32,10 +32,10 @@
<tr> <tr>
<td> <td>
<strong>{{ 'contact'|trans }}</strong>: <strong>{{ 'contact'|trans }}</strong>:
{{ model.template.contact|nl2str(' &ndash; ') }} {{ model.template.contact|nl2str(' ') }}
<br> <br>
<strong>{{ 'invoice_bank_account'|trans }}</strong>: <strong>{{ 'invoice_bank_account'|trans }}</strong>:
{{ model.template.paymentDetails|nl2str(' &ndash; ') }} {{ model.template.paymentDetails|nl2str(' ') }}
</td> </td>
<td align="right"> <td align="right">
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }} {{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}

View File

@@ -138,11 +138,11 @@
<footer class="footer"> <footer class="footer">
<p> <p>
<strong>{{ 'address'|trans }}</strong>: {{ model.template.company }} &ndash; {{ model.template.address|nl2str(' &ndash; ') }} <strong>{{ 'address'|trans }}</strong>: {{ model.template.company }} &ndash; {{ model.template.address|nl2str(' ') }}
<br> <br>
<strong>{{ 'invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|nl2str(' &ndash; ') }} <strong>{{ 'invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|nl2str(' ') }}
<br> <br>
<strong>{{ 'contact'|trans }}</strong>: {{ model.template.contact|nl2str(' &ndash; ') }} <strong>{{ 'contact'|trans }}</strong>: {{ model.template.contact|nl2str(' ') }}
</p> </p>
</footer> </footer>
{% endblock %} {% endblock %}

View File

@@ -18,7 +18,7 @@
<table class="header"> <table class="header">
<tr> <tr>
<td class="text-small"> <td class="text-small">
{{ model.template.company }} &ndash; {{ model.template.address|nl2str(' &ndash; ') }} {{ model.template.company }} &ndash; {{ model.template.address|nl2str(' ') }}
{% if model.template.vatId is not empty %} {% if model.template.vatId is not empty %}
&ndash; {{ 'vat_id'|trans }}: {{ model.template.vatId }} &ndash; {{ 'vat_id'|trans }}: {{ model.template.vatId }}
{% endif %} {% endif %}

View File

@@ -18,11 +18,11 @@
} %} } %}
{% if user.accountNumber is not empty %} {% if user.accountNumber is not empty %}
{% set datagrid = datagrid|merge({ {% set datagrid = datagrid|merge({
'account_number' : (user.accountNumber ?? '&ndash;'), 'account_number' : (user.accountNumber ?? ''),
}) %} }) %}
{% endif %} {% endif %}
{% set datagrid = datagrid|merge({ {% set datagrid = datagrid|merge({
'profile.first_entry' : (firstTimesheet is not null ? firstTimesheet|date_short : '&ndash;'), 'profile.first_entry' : (firstTimesheet is not null ? firstTimesheet|date_short : ''),
'profile.registration_date' : user.registeredAt|date_short, 'profile.registration_date' : user.registeredAt|date_short,
}) %} }) %}
{% set seeOwnRate = false %} {% set seeOwnRate = false %}

Some files were not shown because too many files have changed in this diff Show More