Skip to content

Commit

Permalink
Organization members (#129)
Browse files Browse the repository at this point in the history
* Allow to manage organization members in UI (#118)

* Create owner role and prepare migration (#127)

* Implement members permissions (#128)

* Fix doctrine proxy and UI for organization members (#130)
  • Loading branch information
akondas committed May 4, 2020
1 parent 9d4e14e commit a4703ee
Show file tree
Hide file tree
Showing 60 changed files with 1,251 additions and 154 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
"@phpstan",
"@coverage-ci",
"bin/console lint:twig templates --show-deprecations",
"rm -rf var/cache/prod",
"bin/console cache:warmup --env=prod"
]
}
Expand Down
2 changes: 1 addition & 1 deletion config/packages/doctrine.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ doctrine:
url: '%env(resolve:DATABASE_URL)%'
types:
uuid: 'Ramsey\Uuid\Doctrine\UuidType'
schema_filter: '~^(?!messenger_messages|organization_package_webhook_request|proxy_package_download)~'
schema_filter: '~^(?!messenger_messages|organization_package_webhook_request|proxy_package_download|lock_keys)~'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
parameters:
excludes_analyse:
- src/Entity/Organization.php
ignoreErrors:
-
message: "#^Strict comparison using \\!\\=\\= between string and null will always evaluate to true\\.$#"
Expand Down
2 changes: 2 additions & 0 deletions src/Controller/OAuth/BitbucketController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Buddy\Repman\Service\BitbucketApi;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use League\OAuth2\Client\Token\AccessToken;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Expand Down Expand Up @@ -46,6 +47,7 @@ public function registerCheck(Request $request, BitbucketApi $api): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/add-from-bitbucket", name="fetch_bitbucket_package_token", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function packageAddFromBitbucket(Organization $organization): Response
Expand Down
2 changes: 2 additions & 0 deletions src/Controller/OAuth/GitHubController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Buddy\Repman\Query\User\Model\Organization;
use Buddy\Repman\Service\GitHubApi;
use League\OAuth2\Client\Token\AccessToken;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Expand Down Expand Up @@ -48,6 +49,7 @@ public function registerCheck(Request $request, GitHubApi $api): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/add-from-github", name="fetch_github_package_token", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function packageAddFromGithub(Organization $organization): Response
Expand Down
2 changes: 2 additions & 0 deletions src/Controller/OAuth/GitLabController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Buddy\Repman\Query\User\Model\Organization;
use League\OAuth2\Client\Token\AccessToken;
use Omines\OAuth2\Client\Provider\GitlabResourceOwner;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
Expand Down Expand Up @@ -50,6 +51,7 @@ function (): string {
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/add-from-gitlab", name="fetch_gitlab_package_token", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function packageAddFromGitLab(Organization $organization): Response
Expand Down
134 changes: 134 additions & 0 deletions src/Controller/Organization/MembersController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Controller\Organization;

use Buddy\Repman\Entity\User;
use Buddy\Repman\Form\Type\Organization\InviteMemberType;
use Buddy\Repman\Message\Organization\Member\AcceptInvitation;
use Buddy\Repman\Message\Organization\Member\InviteUser;
use Buddy\Repman\Message\Organization\Member\RemoveInvitation;
use Buddy\Repman\Message\Organization\Member\RemoveMember;
use Buddy\Repman\Query\Admin\Model\User as UserReadModel;
use Buddy\Repman\Query\User\Model\Organization;
use Buddy\Repman\Query\User\Model\Organization\Invitation;
use Buddy\Repman\Query\User\OrganizationQuery;
use Ramsey\Uuid\Uuid;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

final class MembersController extends AbstractController
{
private OrganizationQuery $organizations;
private TokenStorageInterface $tokenStorage;

public function __construct(OrganizationQuery $organizations, TokenStorageInterface $tokenStorage)
{
$this->organizations = $organizations;
$this->tokenStorage = $tokenStorage;
}

/**
* @Route("/organization/{organization}/member", name="organization_members", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function listMembers(Organization $organization, Request $request): Response
{
return $this->render('organization/member/members.html.twig', [
'organization' => $organization,
'members' => $this->organizations->findAllMembers($organization->id(), 20, (int) $request->get('offset', 0)),
'count' => $this->organizations->membersCount($organization->id()),
'invitations' => $this->organizations->invitationsCount($organization->id()),
]);
}

/**
* @Route("/user/invitation/{token}", name="organization_accept_invitation", methods={"GET"}, requirements={"token"="%uuid_pattern%"})
*/
public function acceptInvitation(string $token): Response
{
/** @var User $user */
$user = $this->getUser();
$organization = $this->organizations->getByInvitation($token, $user->getEmail());
if ($organization->isEmpty()) {
$this->addFlash('danger', 'Invitation not found or belongs to different user');
$this->tokenStorage->setToken();
throw new AuthenticationException();
}

$this->dispatchMessage(new AcceptInvitation($token, $user->id()->toString()));
$this->addFlash('success', sprintf('The invitation to %s organization has been accepted', $organization->get()->name()));

return $this->redirectToRoute('organization_overview', ['organization' => $organization->get()->alias()]);
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/member/invite", name="organization_invite_member", methods={"GET", "POST"}, requirements={"organization"="%organization_pattern%"})
*/
public function invite(Organization $organization, Request $request): Response
{
$form = $this->createForm(InviteMemberType::class, [], ['organizationId' => $organization->id()]);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
$this->dispatchMessage(new InviteUser(
$email = $form->get('email')->getData(),
$form->get('role')->getData(),
$organization->id(),
Uuid::uuid4()->toString()
));

$this->addFlash('success', sprintf('User "%s" has been successfully invited.', $email));

return $this->redirectToRoute('organization_invitations', ['organization' => $organization->alias()]);
}

return $this->render('organization/member/invite.twig', [
'organization' => $organization,
'form' => $form->createView(),
]);
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/invitation", name="organization_invitations", methods={"GET"}, requirements={"organization"="%organization_pattern%"})
*/
public function listInvitations(Organization $organization, Request $request): Response
{
return $this->render('organization/member/invitations.html.twig', [
'organization' => $organization,
'invitations' => $this->organizations->findAllInvitations($organization->id(), 20, (int) $request->get('offset', 0)),
'count' => $this->organizations->invitationsCount($organization->id()),
]);
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/invitation/{token}", name="organization_remove_invitation", methods={"DELETE"}, requirements={"organization"="%organization_pattern%"})
*/
public function removeInvitation(Organization $organization, string $token): Response
{
$this->dispatchMessage(new RemoveInvitation($organization->id(), $token));
$this->addFlash('success', 'The invitation has been deleted');

return $this->redirectToRoute('organization_invitations', ['organization' => $organization->alias()]);
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/member/{user}", name="organization_remove_member", methods={"DELETE"}, requirements={"organization"="%organization_pattern%"})
*/
public function removeMember(Organization $organization, UserReadModel $user): Response
{
$this->dispatchMessage(new RemoveMember($organization->id(), $user->id()));
$this->addFlash('success', sprintf('Member "%s" has been removed from organization', $user->email()));

return $this->redirectToRoute('organization_members', ['organization' => $organization->alias()]);
}
}
2 changes: 2 additions & 0 deletions src/Controller/Organization/PackageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Buddy\Repman\Service\GitLabApi;
use Http\Client\Exception as HttpException;
use Ramsey\Uuid\Uuid;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
Expand All @@ -33,6 +34,7 @@
final class PackageController extends AbstractController
{
/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/new/{type?}", name="organization_package_new", methods={"GET","POST"}, requirements={"organization"="%organization_pattern%"})
*/
public function packageNew(Organization $organization, Request $request, GithubApi $githubApi, GitlabApi $gitlabApi, BitbucketApi $bitbucketApi, ?string $type): Response
Expand Down
6 changes: 5 additions & 1 deletion src/Controller/OrganizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Buddy\Repman\Service\ExceptionHandler;
use Buddy\Repman\Service\Organization\AliasGenerator;
use Ramsey\Uuid\Uuid;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand Down Expand Up @@ -95,7 +96,7 @@ public function overview(Organization $organization): Response
public function packages(Organization $organization, Request $request): Response
{
$count = $this->packageQuery->count($organization->id());
if ($count === 0) {
if ($count === 0 && $organization->isOwner($this->getUser()->id()->toString())) {
return $this->redirectToRoute('organization_package_new', ['organization' => $organization->alias()]);
}

Expand All @@ -119,6 +120,7 @@ public function updatePackage(Organization $organization, Package $package): Res
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/package/{package}", name="organization_package_remove", methods={"DELETE"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"})
*/
public function removePackage(Organization $organization, Package $package): Response
Expand Down Expand Up @@ -247,6 +249,7 @@ public function removeToken(Organization $organization, string $token): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}/settings", name="organization_settings", methods={"GET","POST"}, requirements={"organization"="%organization_pattern%"})
*/
public function settings(Organization $organization, Request $request): Response
Expand Down Expand Up @@ -277,6 +280,7 @@ public function settings(Organization $organization, Request $request): Response
}

/**
* @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization")
* @Route("/organization/{organization}", name="organization_remove", methods={"DELETE"}, requirements={"organization"="%organization_pattern%"})
*/
public function removeOrganization(Organization $organization): Response
Expand Down

0 comments on commit a4703ee

Please sign in to comment.