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

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

View File

@@ -26,15 +26,15 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/activities')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Activity')]
final class ActivityController extends BaseApiController
{
@@ -179,7 +179,7 @@ final class ActivityController extends BaseApiController
/**
* 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\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityEditForm'))]
#[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
*/
#[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\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+'])]
@@ -250,7 +250,7 @@ final class ActivityController extends BaseApiController
/**
* 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\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+'])]
@@ -269,15 +269,14 @@ final class ActivityController extends BaseApiController
/**
* 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\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)]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[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, ActivityRate $rate): Response
public function deleteRateAction(Activity $activity, #[MapEntity(mapping: ['rateId' => 'id'])] ActivityRate $rate): Response
{
if ($rate->getActivity() !== $activity) {
throw $this->createNotFoundException();
@@ -293,7 +292,7 @@ final class ActivityController extends BaseApiController
/**
* 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\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'))]

View File

@@ -17,10 +17,10 @@ use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
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')]
final class ConfigurationController extends BaseApiController
{

View File

@@ -26,15 +26,15 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/customers')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Customer')]
final class CustomerController extends BaseApiController
{
@@ -156,7 +156,7 @@ final class CustomerController extends BaseApiController
/**
* 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\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerEditForm'))]
#[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
*/
#[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\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+'])]
@@ -227,7 +227,7 @@ final class CustomerController extends BaseApiController
/**
* 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\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+'])]
@@ -246,15 +246,14 @@ final class CustomerController extends BaseApiController
/**
* 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\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)]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[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, CustomerRate $rate): Response
public function deleteRateAction(Customer $customer, #[MapEntity(mapping: ['rateId' => 'id'])] CustomerRate $rate): Response
{
if ($rate->getCustomer() !== $customer) {
throw $this->createNotFoundException();
@@ -270,7 +269,7 @@ final class CustomerController extends BaseApiController
/**
* 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\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'))]

View File

@@ -27,16 +27,16 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Validator\Constraints;
#[Route(path: '/projects')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Project')]
final class ProjectController extends BaseApiController
{
@@ -209,7 +209,7 @@ final class ProjectController extends BaseApiController
/**
* 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\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectEditForm'))]
#[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
*/
#[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\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+'])]
@@ -282,7 +282,7 @@ final class ProjectController extends BaseApiController
/**
* 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\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+'])]
@@ -301,15 +301,14 @@ final class ProjectController extends BaseApiController
/**
* 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\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)]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[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, ProjectRate $rate): Response
public function deleteRateAction(Project $project, #[MapEntity(mapping: ['rateId' => 'id'])] ProjectRate $rate): Response
{
if ($rate->getProject() !== $project) {
throw $this->createNotFoundException();
@@ -325,7 +324,7 @@ final class ProjectController extends BaseApiController
/**
* 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\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'))]

View File

@@ -16,8 +16,8 @@ use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\ConstraintViolationInterface;
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\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
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')]
final class StatusController extends BaseApiController
{

View File

@@ -18,13 +18,13 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/tags')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Tag')]
final class TagController extends BaseApiController
{
@@ -94,7 +94,7 @@ final class TagController extends BaseApiController
/**
* 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\Parameter(name: 'id', in: 'path', description: 'Tag ID to delete', required: true)]
#[ApiSecurity(name: 'apiUser')]

View File

@@ -21,15 +21,15 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/teams')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Team')]
final class TeamController extends BaseApiController
{
@@ -44,7 +44,7 @@ final class TeamController extends BaseApiController
/**
* 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')))]
#[Rest\Get(path: '', name: 'get_teams')]
#[ApiSecurity(name: 'apiUser')]
@@ -62,7 +62,7 @@ final class TeamController extends BaseApiController
/**
* 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'))]
#[Rest\Get(path: '/{id}', name: 'get_team', requirements: ['id' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
@@ -78,7 +78,7 @@ final class TeamController extends BaseApiController
/**
* Delete a team
*/
#[Security("is_granted('delete_team')")]
#[IsGranted('delete_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)]
#[ApiSecurity(name: 'apiUser')]
@@ -96,7 +96,7 @@ final class TeamController extends BaseApiController
/**
* 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\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))]
#[Rest\Post(path: '', name: 'post_team')]
@@ -127,7 +127,7 @@ final class TeamController extends BaseApiController
/**
* 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\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TeamEditForm'))]
#[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
*/
#[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\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)]
#[Rest\Post(path: '/{id}/members/{userId}', name: 'post_team_member', requirements: ['id' => '\d+', 'userId' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[Entity('member', expr: 'repository.find(userId)')]
public function postMemberAction(Team $team, User $member): Response
public function postMemberAction(Team $team, #[MapEntity(mapping: ['userId' => 'id'])] User $member): Response
{
if ($member->isInTeam($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
*/
#[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\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)]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[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, User $member): Response
public function deleteMemberAction(Team $team, #[MapEntity(mapping: ['userId' => 'id'])] User $member): Response
{
if (!$member->isInTeam($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
*/
#[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\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)]
#[Rest\Post(path: '/{id}/customers/{customerId}', name: 'post_team_customer', requirements: ['id' => '\d+', 'customerId' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[Entity('customer', expr: 'repository.find(customerId)')]
public function postCustomerAction(Team $team, Customer $customer): Response
public function postCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer): Response
{
if ($team->hasCustomer($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
*/
#[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\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)]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[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, Customer $customer): Response
public function deleteCustomerAction(Team $team, #[MapEntity(mapping: ['customerId' => 'id'])] Customer $customer): Response
{
if (!$team->hasCustomer($customer)) {
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
*/
#[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\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)]
#[Rest\Post(path: '/{id}/projects/{projectId}', name: 'post_team_project', requirements: ['id' => '\d+', 'projectId' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[Entity('project', expr: 'repository.find(projectId)')]
public function postProjectAction(Team $team, Project $project): Response
public function postProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project): Response
{
if ($team->hasProject($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
*/
#[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\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)]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[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, Project $project): Response
public function deleteProjectAction(Team $team, #[MapEntity(mapping: ['projectId' => 'id'])] Project $project): Response
{
if (!$team->hasProject($project)) {
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
*/
#[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\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)]
#[Rest\Post(path: '/{id}/activities/{activityId}', name: 'post_team_activity', requirements: ['id' => '\d+', 'activityId' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[Entity('activity', expr: 'repository.find(activityId)')]
public function postActivityAction(Team $team, Activity $activity): Response
public function postActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity): Response
{
if ($team->hasActivity($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
*/
#[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\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)]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[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, Activity $activity): Response
public function deleteActivityAction(Team $team, #[MapEntity(mapping: ['activityId' => 'id'])] Activity $activity): Response
{
if (!$team->hasActivity($activity)) {
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 Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Validator\Constraints;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[Route(path: '/timesheets')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'Timesheet')]
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)
*/
#[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')))]
#[Rest\Get(path: '', name: 'get_timesheets')]
#[ApiSecurity(name: 'apiUser')]
@@ -256,7 +257,7 @@ final class TimesheetController extends BaseApiController
/**
* 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\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to fetch', required: true)]
#[Rest\Get(path: '/{id}', name: 'get_timesheet', requirements: ['id' => '\d+'])]
@@ -273,7 +274,7 @@ final class TimesheetController extends BaseApiController
/**
* 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\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
#[Rest\Post(path: '', name: 'post_timesheet')]
@@ -328,7 +329,7 @@ final class TimesheetController extends BaseApiController
/**
* 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\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'))]
@@ -373,7 +374,7 @@ final class TimesheetController extends BaseApiController
/**
* 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\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to delete', required: true)]
#[ApiSecurity(name: 'apiUser')]
@@ -391,7 +392,7 @@ final class TimesheetController extends BaseApiController
/**
* 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')))]
#[Rest\Get(path: '/recent', name: 'recent_timesheet')]
#[ApiSecurity(name: 'apiUser')]
@@ -427,7 +428,7 @@ final class TimesheetController extends BaseApiController
/**
* 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')))]
#[Rest\Get(path: '/active', name: 'active_timesheet')]
#[ApiSecurity(name: 'apiUser')]
@@ -448,7 +449,7 @@ final class TimesheetController extends BaseApiController
/**
* 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\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to stop', required: true)]
#[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
*/
#[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\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)]
#[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
*/
#[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\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to duplicate', required: true)]
#[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
*/
#[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\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+'])]
@@ -577,7 +578,7 @@ final class TimesheetController extends BaseApiController
/**
* 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\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+'])]

View File

@@ -23,16 +23,16 @@ use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use OpenApi\Attributes as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/users')]
#[Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
#[OA\Tag(name: 'User')]
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)
*/
#[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')))]
#[Rest\Get(path: '', name: 'get_users')]
#[ApiSecurity(name: 'apiUser')]
@@ -95,7 +95,7 @@ final class UserController extends BaseApiController
/**
* 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\Parameter(name: 'id', in: 'path', description: 'User ID to fetch', required: true)]
#[Rest\Get(path: '/{id}', name: 'get_user', requirements: ['id' => '\d+'])]
@@ -131,7 +131,7 @@ final class UserController extends BaseApiController
/**
* Creates a new user
*/
#[Security("is_granted('create_user')")]
#[IsGranted('create_user')]
#[OA\Post(description: 'Creates a new user and returns it afterwards')]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/UserCreateForm'))]
#[Rest\Post(path: '', name: 'post_user')]
@@ -184,7 +184,7 @@ final class UserController extends BaseApiController
/**
* 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\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/UserEditForm'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'User ID to update', required: true)]