API methods to handle teams and users (#1384)
This commit is contained in:
@@ -11,8 +11,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Form\API\TeamApiEditForm;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
@@ -20,23 +27,25 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
/**
|
||||
* @RouteResource("Team")
|
||||
*
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
class TeamController extends BaseApiController
|
||||
final class TeamController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var TeamRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
private $viewHandler;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TeamRepository $repository)
|
||||
{
|
||||
@@ -135,4 +144,481 @@ class TeamController extends BaseApiController
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new team
|
||||
*
|
||||
* @SWG\Post(
|
||||
* description="Creates a new team and returns it afterwards",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the new created team",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity",),
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/TeamEditForm")
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('create_team')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function postAction(Request $request): Response
|
||||
{
|
||||
$team = new Team();
|
||||
$team->setTeamLead($this->getUser());
|
||||
|
||||
$form = $this->createForm(TeamApiEditForm::class, $team);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
$team->addUser($team->getTeamLead());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$view = new View($form);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing team
|
||||
*
|
||||
* @SWG\Patch(
|
||||
* description="Update an existing team, you can pass all or just a subset of all attributes (passing users will replace all existing ones)",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the updated team",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/TeamEditForm")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Team ID to update",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('edit_team')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function patchAction(Request $request, int $id): Response
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$form = $this->createForm(TeamApiEditForm::class, $team);
|
||||
|
||||
$form->setData($team);
|
||||
$form->submit($request->request->all(), false);
|
||||
$team->addUser($team->getTeamLead());
|
||||
|
||||
if (false === $form->isValid()) {
|
||||
$view = new View($form, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new member to a team
|
||||
*
|
||||
* @SWG\Post(
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Adds a new user to a team. The user must not be deactivated.",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The team which will receive the new member",
|
||||
* required=true,
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="userId",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The team member to add (User ID)",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('edit_team')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function postMemberAction(int $id, int $userId, UserRepository $repository): Response
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException('Team not found');
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $repository->find($userId);
|
||||
|
||||
if (null === $user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
if (!$user->isEnabled()) {
|
||||
throw new BadRequestHttpException('Cannot add disabled user to team');
|
||||
}
|
||||
|
||||
if ($user->isInTeam($team)) {
|
||||
throw new BadRequestHttpException('User is already member of the team');
|
||||
}
|
||||
|
||||
$team->addUser($user);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a member from the team
|
||||
*
|
||||
* @SWG\Delete(
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Removes a user from the team. The teamlead cannot be removed.",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The team from which the member will be removed",
|
||||
* required=true,
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="userId",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The team member to remove (User ID)",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('edit_team')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function deleteMemberAction(int $id, int $userId, UserRepository $repository): Response
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException('Team not found');
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $repository->find($userId);
|
||||
|
||||
if (null === $user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
if (!$user->isInTeam($team)) {
|
||||
throw new BadRequestHttpException('User is not a member of the team');
|
||||
}
|
||||
|
||||
if ($team->isTeamlead($user)) {
|
||||
throw new BadRequestHttpException('Cannot remove teamlead');
|
||||
}
|
||||
|
||||
$team->removeUser($user);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant the team access to a customer
|
||||
*
|
||||
* @SWG\Post(
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Adds a new customer to a team. The customer must not be invisible.",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The team that is granted access",
|
||||
* required=true,
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="customerId",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The customer to grant acecess to (Customer ID)",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('edit_team')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function postCustomerAction(int $id, int $customerId, CustomerRepository $repository): Response
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException('Team not found');
|
||||
}
|
||||
|
||||
/** @var Customer $customer */
|
||||
$customer = $repository->find($customerId);
|
||||
|
||||
if (null === $customer) {
|
||||
throw new NotFoundException('Customer not found');
|
||||
}
|
||||
|
||||
if (!$customer->isVisible()) {
|
||||
throw new BadRequestHttpException('Cannot grant access to an invisible customer');
|
||||
}
|
||||
|
||||
if ($team->hasCustomer($customer)) {
|
||||
throw new BadRequestHttpException('Team has already access to customer');
|
||||
}
|
||||
|
||||
$team->addCustomer($customer);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes access for a customer from a team
|
||||
*
|
||||
* @SWG\Delete(
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Removes a customer from the team.",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The team whose permission will be revoked",
|
||||
* required=true,
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="customerId",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The customer to remove (Customer ID)",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('edit_team')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function deleteCustomerAction(int $id, int $customerId, CustomerRepository $repository): Response
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException('Team not found');
|
||||
}
|
||||
|
||||
/** @var Customer $customer */
|
||||
$customer = $repository->find($customerId);
|
||||
|
||||
if (null === $customer) {
|
||||
throw new NotFoundException('Customer not found');
|
||||
}
|
||||
|
||||
if (!$team->hasCustomer($customer)) {
|
||||
throw new BadRequestHttpException('Customer is not assigned to the team');
|
||||
}
|
||||
|
||||
$team->removeCustomer($customer);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant the team access to a project
|
||||
*
|
||||
* @SWG\Post(
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Adds a new project to a team. The project must not be invisible.",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The team that is granted access",
|
||||
* required=true,
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="projectId",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The project to grant acecess to (Project ID)",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('edit_team')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function postProjectAction(int $id, int $projectId, ProjectRepository $repository): Response
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException('Team not found');
|
||||
}
|
||||
|
||||
/** @var Project $project */
|
||||
$project = $repository->find($projectId);
|
||||
|
||||
if (null === $project) {
|
||||
throw new NotFoundException('Project not found');
|
||||
}
|
||||
|
||||
if (!$project->isVisible()) {
|
||||
throw new BadRequestHttpException('Cannot grant access to an invisible project');
|
||||
}
|
||||
|
||||
if ($team->hasProject($project)) {
|
||||
throw new BadRequestHttpException('Team has already access to project');
|
||||
}
|
||||
|
||||
$team->addProject($project);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes access for a project from a team
|
||||
*
|
||||
* @SWG\Delete(
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Removes a project from the team.",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The team whose permission will be revoked",
|
||||
* required=true,
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="projectId",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="The project to remove (Project ID)",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('edit_team')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function deleteProjectAction(int $id, int $projectId, ProjectRepository $repository): Response
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException('Team not found');
|
||||
}
|
||||
|
||||
/** @var Project $project */
|
||||
$project = $repository->find($projectId);
|
||||
|
||||
if (null === $project) {
|
||||
throw new NotFoundException('Project not found');
|
||||
}
|
||||
|
||||
if (!$team->hasProject($project)) {
|
||||
throw new BadRequestHttpException('Project is not assigned to the team');
|
||||
}
|
||||
|
||||
$team->removeProject($project);
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
|
||||
$view = new View($team, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Form\API\UserApiCreateForm;
|
||||
use App\Form\API\UserApiEditForm;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Utils\SearchTerm;
|
||||
@@ -22,34 +26,45 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
/**
|
||||
* @RouteResource("User")
|
||||
*
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
class UserController extends BaseApiController
|
||||
final class UserController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
private $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
private $viewHandler;
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
private $encoder;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param UserRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository)
|
||||
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository, UserPasswordEncoderInterface $encoder, FormConfiguration $config)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->encoder = $encoder;
|
||||
$this->configuration = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,4 +174,125 @@ class UserController extends BaseApiController
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
*
|
||||
* @SWG\Post(
|
||||
* description="Creates a new user and returns it afterwards",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the new created user",
|
||||
* @SWG\Schema(ref="#/definitions/UserEntity",),
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/UserCreateForm")
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('create_user')")
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function postAction(Request $request): Response
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$user->setRoles([User::DEFAULT_ROLE]);
|
||||
$user->setTimezone($this->configuration->getUserDefaultTimezone());
|
||||
$user->setLanguage($this->configuration->getUserDefaultLanguage());
|
||||
|
||||
$form = $this->createForm(UserApiCreateForm::class, $user, [
|
||||
'include_roles' => $this->isGranted('roles', $user),
|
||||
'include_active_flag' => true,
|
||||
'include_preferences' => $this->isGranted('preferences', $user),
|
||||
]);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$password = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($password);
|
||||
|
||||
$this->repository->saveUser($user);
|
||||
|
||||
$view = new View($user, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$view = new View($form);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing user
|
||||
*
|
||||
* @SWG\Patch(
|
||||
* description="Update an existing user, you can pass all or just a subset of all attributes (passing roles will replace all existing ones)",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the updated user",
|
||||
* @SWG\Schema(ref="#/definitions/UserEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/UserEditForm")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="User ID to update",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @ApiSecurity(name="apiUser")
|
||||
* @ApiSecurity(name="apiToken")
|
||||
*/
|
||||
public function patchAction(Request $request, int $id): Response
|
||||
{
|
||||
$user = $this->repository->getUserById($id);
|
||||
|
||||
if (null === $user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $user)) {
|
||||
throw new AccessDeniedHttpException('Not allowed to edit user');
|
||||
}
|
||||
|
||||
$form = $this->createForm(UserApiEditForm::class, $user, [
|
||||
'include_roles' => $this->isGranted('roles', $user),
|
||||
'include_active_flag' => ($user->getId() !== $this->getUser()->getId()),
|
||||
'include_preferences' => $this->isGranted('preferences', $user),
|
||||
]);
|
||||
|
||||
$form->setData($user);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
if (false === $form->isValid()) {
|
||||
$view = new View($form, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$this->repository->saveUser($user);
|
||||
|
||||
$view = new View($user, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ use App\Form\UserRolesType;
|
||||
use App\Form\UserTeamsType;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Voter\UserVoter;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -281,23 +280,23 @@ class ProfileController extends AbstractController
|
||||
): Response {
|
||||
$forms = [];
|
||||
|
||||
if ($this->isGranted(UserVoter::EDIT, $user)) {
|
||||
if ($this->isGranted('edit', $user)) {
|
||||
$editForm = $editForm ?: $this->createEditForm($user);
|
||||
$forms['settings'] = $editForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::PASSWORD, $user)) {
|
||||
if ($this->isGranted('password', $user)) {
|
||||
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
|
||||
$forms['password'] = $pwdForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::API_TOKEN, $user)) {
|
||||
if ($this->isGranted('api-token', $user)) {
|
||||
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
|
||||
$forms['api-token'] = $apiTokenForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::TEAMS, $user) && $this->teams->count([]) > 0) {
|
||||
if ($this->isGranted('teams', $user) && $this->teams->count([]) > 0) {
|
||||
$teamsForm = $teamsForm ?: $this->createTeamsForm($user);
|
||||
$forms['teams'] = $teamsForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::ROLES, $user)) {
|
||||
if ($this->isGranted('roles', $user)) {
|
||||
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
||||
$forms['roles'] = $rolesForm->createView();
|
||||
}
|
||||
@@ -329,7 +328,8 @@ class ProfileController extends AbstractController
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_edit', ['username' => $user->getUsername()]),
|
||||
'method' => 'POST',
|
||||
'include_active_flag' => ($user->getId() !== $this->getUser()->getId())
|
||||
'include_active_flag' => ($user->getId() !== $this->getUser()->getId()),
|
||||
'include_preferences' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Form\Toolbar\UserToolbarForm;
|
||||
@@ -31,20 +32,20 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
* @Route(path="/admin/user")
|
||||
* @Security("is_granted('view_user')")
|
||||
*/
|
||||
class UserController extends AbstractController
|
||||
final class UserController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
protected $encoder;
|
||||
private $encoder;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, UserRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
@@ -93,23 +94,31 @@ class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
private function createNewDefaultUser(FormConfiguration $config): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$user->setRoles([User::DEFAULT_ROLE]);
|
||||
$user->setTimezone($config->getUserDefaultTimezone());
|
||||
$user->setLanguage($config->getUserDefaultLanguage());
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_user')")
|
||||
*/
|
||||
public function createAction(Request $request): Response
|
||||
public function createAction(Request $request, FormConfiguration $config): Response
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$editForm = $this->createEditForm($user);
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
$editForm = $this->getCreateUserForm($user);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$password = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($password);
|
||||
$user->setEnabled(true);
|
||||
$user->setRoles([User::DEFAULT_ROLE]);
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($user);
|
||||
@@ -121,8 +130,12 @@ class UserController extends AbstractController
|
||||
return $this->redirectToRoute('user_profile_edit', ['username' => $user->getUsername()]);
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$editForm = $this->createEditForm($user);
|
||||
$firstUser = $user;
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
$user->setLanguage($firstUser->getLanguage());
|
||||
$user->setTimezone($firstUser->getTimezone());
|
||||
|
||||
$editForm = $this->getCreateUserForm($user);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
}
|
||||
|
||||
@@ -187,12 +200,14 @@ class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
private function createEditForm(User $user): FormInterface
|
||||
private function getCreateUserForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(UserCreateType::class, $user, [
|
||||
'action' => $this->generateUrl('admin_user_create'),
|
||||
'method' => 'POST',
|
||||
'include_active_flag' => true
|
||||
'include_active_flag' => true,
|
||||
'include_preferences' => $this->isGranted('preferences', $user),
|
||||
'include_add_more' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,11 @@ class Team
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
public function hasCustomer(Customer $customer): bool
|
||||
{
|
||||
return $this->customers->contains($customer);
|
||||
}
|
||||
|
||||
public function addCustomer(Customer $customer)
|
||||
{
|
||||
if ($this->customers->contains($customer)) {
|
||||
@@ -172,6 +177,11 @@ class Team
|
||||
return $this->customers;
|
||||
}
|
||||
|
||||
public function hasProject(Project $project): bool
|
||||
{
|
||||
return $this->projects->contains($project);
|
||||
}
|
||||
|
||||
public function addProject(Project $project)
|
||||
{
|
||||
if ($this->projects->contains($project)) {
|
||||
|
||||
@@ -266,6 +266,27 @@ class User extends BaseUser implements UserInterface
|
||||
return $this->getPreferenceValue(UserPreference::TIMEZONE, date_default_timezone_get());
|
||||
}
|
||||
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->getLocale();
|
||||
}
|
||||
|
||||
public function setLanguage(?string $language)
|
||||
{
|
||||
if ($language === null) {
|
||||
$language = User::DEFAULT_LANGUAGE;
|
||||
}
|
||||
$this->setPreferenceValue(UserPreference::LOCALE, $language);
|
||||
}
|
||||
|
||||
public function setTimezone(?string $timezone)
|
||||
{
|
||||
if ($timezone === null) {
|
||||
$timezone = date_default_timezone_get();
|
||||
}
|
||||
$this->setPreferenceValue(UserPreference::TIMEZONE, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $default
|
||||
|
||||
35
src/Form/API/TeamApiEditForm.php
Normal file
35
src/Form/API/TeamApiEditForm.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\API;
|
||||
|
||||
use App\Form\TeamEditForm;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamApiEditForm extends TeamEditForm
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'expand_users' => false,
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
58
src/Form/API/UserApiCreateForm.php
Normal file
58
src/Form/API/UserApiCreateForm.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\API;
|
||||
|
||||
use App\Form\Type\UserRoleType;
|
||||
use App\Form\UserCreateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserApiCreateForm extends UserCreateType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder->remove('plainPassword');
|
||||
|
||||
$builder->add('plainPassword', PasswordType::class, [
|
||||
'required' => true,
|
||||
'label' => 'label.password',
|
||||
'documentation' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Plain text password',
|
||||
],
|
||||
]);
|
||||
|
||||
if ($options['include_roles']) {
|
||||
$builder->add('roles', UserRoleType::class, [
|
||||
'label' => 'label.roles',
|
||||
'required' => false,
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
'include_roles' => true,
|
||||
'include_add_more' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
src/Form/API/UserApiEditForm.php
Normal file
45
src/Form/API/UserApiEditForm.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\API;
|
||||
|
||||
use App\Form\Type\UserRoleType;
|
||||
use App\Form\UserEditType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserApiEditForm extends UserEditType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
if ($options['include_roles']) {
|
||||
$builder->add('roles', UserRoleType::class, [
|
||||
'label' => 'label.roles',
|
||||
'required' => false,
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
'include_roles' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -29,15 +29,25 @@ class TeamEditForm extends AbstractType
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
// documentation is for NelmioApiDocBundle
|
||||
'documentation' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Name of the new team',
|
||||
],
|
||||
])
|
||||
->add('teamlead', UserType::class, [
|
||||
'label' => 'label.teamlead',
|
||||
'multiple' => false,
|
||||
'expanded' => false,
|
||||
// documentation is for NelmioApiDocBundle
|
||||
'documentation' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'User ID for the teamlead',
|
||||
],
|
||||
])
|
||||
->add('users', UserType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'expanded' => $options['expand_users'],
|
||||
'by_reference' => false,
|
||||
])
|
||||
;
|
||||
@@ -52,6 +62,7 @@ class TeamEditForm extends AbstractType
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'expand_users' => true,
|
||||
'csrf_token_id' => 'admin_team_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
@@ -43,20 +42,21 @@ class UserCreateType extends UserEditType
|
||||
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder->add('create_more', CheckboxType::class, [
|
||||
'label' => 'label.create_more',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
]);
|
||||
if ($options['include_add_more'] === true) {
|
||||
$builder->add('create_more', CheckboxType::class, [
|
||||
'label' => 'label.create_more',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'class' => User::class,
|
||||
'include_add_more' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\AvatarType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -45,6 +47,16 @@ class UserEditType extends AbstractType
|
||||
])
|
||||
;
|
||||
|
||||
if ($options['include_preferences']) {
|
||||
$builder->add('language', LanguageType::class, [
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$builder->add('timezone', TimezoneType::class, [
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($options['include_active_flag']) {
|
||||
$builder
|
||||
->add('enabled', YesNoType::class, [
|
||||
@@ -64,7 +76,8 @@ class UserEditType extends AbstractType
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'edit_user_profile',
|
||||
'include_active_flag' => false,
|
||||
'include_active_flag' => true,
|
||||
'include_preferences' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\UserFormTypeQuery;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
@@ -32,7 +33,19 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fetch the currently logged-in user.
|
||||
* @param User $user
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveUser(User $user)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fetch a user by its ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @return null|User
|
||||
|
||||
@@ -15,17 +15,13 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
class TeamVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Team)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::DELETE,
|
||||
'view',
|
||||
'edit',
|
||||
'delete',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,26 +17,16 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
*/
|
||||
class UserVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
public const PASSWORD = 'password';
|
||||
public const ROLES = 'roles';
|
||||
public const TEAMS = 'teams';
|
||||
public const PREFERENCES = 'preferences';
|
||||
public const API_TOKEN = 'api-token';
|
||||
public const HOURLY_RATE = 'hourly-rate';
|
||||
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::ROLES,
|
||||
self::TEAMS,
|
||||
self::PASSWORD,
|
||||
self::DELETE,
|
||||
self::PREFERENCES,
|
||||
self::API_TOKEN,
|
||||
self::HOURLY_RATE,
|
||||
'view',
|
||||
'edit',
|
||||
'roles',
|
||||
'teams',
|
||||
'password',
|
||||
'delete',
|
||||
'preferences',
|
||||
'api-token',
|
||||
'hourly-rate',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -71,39 +61,21 @@ class UserVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
$permission = '';
|
||||
|
||||
switch ($attribute) {
|
||||
// special case for the UserController
|
||||
case self::DELETE:
|
||||
if ($subject->getId() === $user->getId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, 'delete_user');
|
||||
|
||||
case self::VIEW:
|
||||
case self::EDIT:
|
||||
case self::PREFERENCES:
|
||||
case self::PASSWORD:
|
||||
case self::API_TOKEN:
|
||||
case self::ROLES:
|
||||
case self::TEAMS:
|
||||
case self::HOURLY_RATE:
|
||||
$permission .= $attribute;
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($attribute === 'delete') {
|
||||
if ($subject->getId() === $user->getId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, 'delete_user');
|
||||
}
|
||||
|
||||
$permission .= '_';
|
||||
$permission = $attribute;
|
||||
|
||||
// extend me for "team" support later on
|
||||
if ($subject->getId() == $user->getId()) {
|
||||
$permission .= 'own';
|
||||
if ($subject->getId() === $user->getId()) {
|
||||
$permission .= '_own';
|
||||
} else {
|
||||
$permission .= 'other';
|
||||
$permission .= '_other';
|
||||
}
|
||||
|
||||
$permission .= '_profile';
|
||||
|
||||
Reference in New Issue
Block a user