Skip to content

Commit

Permalink
IBX-2493: Added option to invite other users to DXP & register from i…
Browse files Browse the repository at this point in the history
…nvitation (#35)

* IBX-2493: Added option for SA aware registered user content type

* IBX-2493: Added possibility of inviting other users to given SA

* IBX-2493: Added limitation system for user / invite

* IBX-1543: Added option to consume invitation

* IBX-2493: Added unit tests for new Limitation and Invitation validation

* IBX-2493: Fixed role filtering

* IBX-2493: Added group filtering

* IBX-2493: Fixed code style

* IBX-2493: Redo UserGroup ChoiceLoader

* IBX-2493: Fixed minor issues after code review

* IBX-2493: Refactored to use create struct

* [CS]: Fixed codestyle

* Used Batch iterator

* Fixed service name

* Used SA identifier instead of full object

* Extracted persistence layer

* Changed properties/classes visibility

* Added minor fixes after review

* Moved stuff around

* Fixed callables
  • Loading branch information
ViniTou authored May 26, 2022
1 parent aae5213 commit fda7714
Show file tree
Hide file tree
Showing 53 changed files with 2,946 additions and 32 deletions.
90 changes: 90 additions & 0 deletions src/bundle/Controller/UserInvitationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\User\Controller;

use Ibexa\Contracts\User\Invitation\Exception\InvitationAlreadyExistsException;
use Ibexa\Contracts\User\Invitation\Exception\UserAlreadyExistsException;
use Ibexa\Contracts\User\Invitation\InvitationCreateStruct;
use Ibexa\Contracts\User\Invitation\InvitationSender;
use Ibexa\Contracts\User\Invitation\InvitationService;
use Ibexa\User\ExceptionHandler\ActionResultHandler;
use Ibexa\User\Form\Type\Invitation\UserInvitationType;
use Ibexa\User\View\Invitation\FormView;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;

final class UserInvitationController extends Controller
{
private InvitationService $invitationService;

private InvitationSender $mailSender;

private FormFactoryInterface $formFactory;

private ActionResultHandler $actionResultHandler;

public function __construct(
InvitationService $invitationService,
InvitationSender $mailSender,
FormFactoryInterface $formFactory,
ActionResultHandler $actionResultHandler
) {
$this->invitationService = $invitationService;
$this->mailSender = $mailSender;
$this->formFactory = $formFactory;
$this->actionResultHandler = $actionResultHandler;
}

public function inviteUser(Request $request): FormView
{
$form = $this->formFactory->create(UserInvitationType::class);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
/** @var \Ibexa\User\Form\Data\UserInvitationData $data */
$data = $form->getData();
try {
$invitation = $this->invitationService->createInvitation(
new InvitationCreateStruct(
$data->getEmail(),
$data->getSiteaccess()->name,
$data->getUserGroup(),
$data->getRole(),
$data->getRoleLimitation(),
)
);

$this->mailSender->sendInvitation($invitation);

$this->actionResultHandler->success(
/** @Desc("Invitation send to '%email%' updated.") */
'user_invitation.send.success',
['%email%' => $data->getEmail()],
'user_invitation'
);
} catch (InvitationAlreadyExistsException $e) {
$this->actionResultHandler->error(
/** @Desc("Invitation for '%email%' already exist.") */
'user_invitation.send.invitation_exist',
['%email%' => $data->getEmail()],
'user_invitation'
);
} catch (UserAlreadyExistsException $e) {
$this->actionResultHandler->error(
/** @Desc("User with '%email%' already exist.") */
'user_invitation.send.user_exist',
['%email%' => $data->getEmail()],
'user_invitation'
);
}
}

return new FormView(null, ['form' => $form->createView()]);
}
}
51 changes: 45 additions & 6 deletions src/bundle/Controller/UserRegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Ibexa\Bundle\User\Controller;

use Ibexa\ContentForms\Form\ActionDispatcher\ActionDispatcherInterface;
use Ibexa\Contracts\User\Invitation\InvitationService;
use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
use Ibexa\User\Form\DataMapper\UserRegisterMapper;
use Ibexa\User\Form\Type\UserRegisterType;
Expand All @@ -25,16 +26,16 @@ class UserRegisterController extends Controller
/** @var \Ibexa\ContentForms\Form\ActionDispatcher\ActionDispatcherInterface */
private $userActionDispatcher;

/**
* @param \Ibexa\User\Form\DataMapper\UserRegisterMapper $userRegisterMapper
* @param \Ibexa\ContentForms\Form\ActionDispatcher\ActionDispatcherInterface $userActionDispatcher
*/
private InvitationService $invitationService;

public function __construct(
UserRegisterMapper $userRegisterMapper,
ActionDispatcherInterface $userActionDispatcher
ActionDispatcherInterface $userActionDispatcher,
InvitationService $invitationService
) {
$this->userRegisterMapper = $userRegisterMapper;
$this->userActionDispatcher = $userActionDispatcher;
$this->invitationService = $invitationService;
}

/**
Expand All @@ -53,7 +54,6 @@ public function registerAction(Request $request)
$data = $this->userRegisterMapper->mapToFormData();
$language = $data->mainLanguageCode;

/** @var \Symfony\Component\Form\Form $form */
$form = $this->createForm(
UserRegisterType::class,
$data,
Expand Down Expand Up @@ -81,6 +81,45 @@ public function registerConfirmAction(): ConfirmView
{
return new ConfirmView();
}

/**
* @return \Ibexa\User\View\Register\FormView|\Symfony\Component\HttpFoundation\Response
*/
public function registerFromInvitationAction(Request $request)
{
$invitation = $this->invitationService->getInvitation($request->get('inviteHash'));

if (!$this->invitationService->isValid($invitation)) {
throw new UnauthorizedHttpException('You are not allowed to register a new account');
}

$this->userRegisterMapper->setParam('invitation', $invitation);
$data = $this->userRegisterMapper->mapToFormData();
$language = $data->mainLanguageCode;

$form = $this->createForm(
UserRegisterType::class,
$data,
[
'languageCode' => $language,
'mainLanguageCode' => $language,
'intent' => 'invitation',
]
);

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid() && null !== $form->getClickedButton()) {
$this->userActionDispatcher->dispatchFormAction($form, $data, $form->getClickedButton()->getName());
if ($response = $this->userActionDispatcher->getResponse()) {
$this->invitationService->markAsUsed($invitation);

return $response;
}
}

return new FormView(null, ['form' => $form->createView()]);
}
}

class_alias(UserRegisterController::class, 'EzSystems\EzPlatformUserBundle\Controller\UserRegisterController');
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\User\DependencyInjection\Configuration\Parser;

use Ibexa\Bundle\Core\DependencyInjection\Configuration\AbstractParser;
use Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware\ContextualizerInterface;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;

/*
* Example configuration:
* ```yaml
* ibexa:
* system:
* default: # configuration per siteaccess or siteaccess group
* user_invitation:
* hash_expiration_time: P1D
* templates:
* mail: "@@App/invitation/mail.html.twig"
* ```
*/
class UserInvitation extends AbstractParser
{
/**
* Adds semantic configuration definition.
*
* @param \Symfony\Component\Config\Definition\Builder\NodeBuilder $nodeBuilder Node just under ezpublish.system.<siteaccess>
*/
public function addSemanticConfig(NodeBuilder $nodeBuilder)
{
$nodeBuilder
->arrayNode('user_invitation')
->info('User invitation configuration')
->children()
->scalarNode('hash_expiration_time')
->defaultValue('P2D')
->end()
->arrayNode('templates')
->info('User invitation templates.')
->children()
->scalarNode('form')
->info('Template to use for registration form rendering.')
->end()
->scalarNode('mail')
->info('Template to use for registration confirmation rendering.')
->end()
->end()
->end()
->end()
->end();
}

public function mapConfig(array &$scopeSettings, $currentScope, ContextualizerInterface $contextualizer)
{
if (empty($scopeSettings['user_invitation'])) {
return;
}

$settings = $scopeSettings['user_invitation'];

if (!empty($settings['hash_expiration_time'])) {
$contextualizer->setContextualParameter(
'user_invitation.hash_expiration_time',
$currentScope,
$settings['hash_expiration_time']
);
}

if (!empty($settings['templates']['form'])) {
$contextualizer->setContextualParameter(
'user_invitation.templates.form',
$currentScope,
$settings['templates']['form']
);
}

if (!empty($settings['templates']['mail'])) {
$contextualizer->setContextualParameter(
'user_invitation.templates.mail',
$currentScope,
$settings['templates']['mail']
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder)
->arrayNode('user_registration')
->info('User registration configuration')
->children()
->scalarNode('user_type_identifier')
->info('Content type identifier used for registration.')
->defaultValue('user')
->end()
->scalarNode('group_id')
->info('Content id of the user group where users who register are created.')
->defaultValue(11)
Expand Down Expand Up @@ -52,6 +56,14 @@ public function mapConfig(array &$scopeSettings, $currentScope, ContextualizerIn

$settings = $scopeSettings['user_registration'];

if (!empty($settings['user_type_identifier'])) {
$contextualizer->setContextualParameter(
'user_registration.user_type_identifier',
$currentScope,
$settings['user_type_identifier']
);
}

if (!empty($settings['group_id'])) {
$contextualizer->setContextualParameter(
'user_registration.group_id',
Expand Down
5 changes: 5 additions & 0 deletions src/bundle/IbexaUserBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
use Ibexa\Bundle\User\DependencyInjection\Configuration\Parser\Pagination;
use Ibexa\Bundle\User\DependencyInjection\Configuration\Parser\ResetPassword;
use Ibexa\Bundle\User\DependencyInjection\Configuration\Parser\Security;
use Ibexa\Bundle\User\DependencyInjection\Configuration\Parser\UserInvitation;
use Ibexa\Bundle\User\DependencyInjection\Configuration\Parser\UserPreferences;
use Ibexa\Bundle\User\DependencyInjection\Configuration\Parser\UserRegistration;
use Ibexa\Bundle\User\DependencyInjection\Configuration\Parser\UserSettingsUpdateView;
use Ibexa\User\Permission\InvitationPolicyProvider;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

Expand All @@ -36,6 +38,9 @@ public function build(ContainerBuilder $container)
$core->addConfigParser(new UserSettingsUpdateView());
$core->addConfigParser(new ForgotPassword());
$core->addConfigParser(new ResetPassword());
$core->addConfigParser(new UserInvitation());

$core->addPolicyProvider(new InvitationPolicyProvider());

$container->addCompilerPass(new UserSetting\ValueDefinitionPass());
$container->addCompilerPass(new UserSetting\FormMapperPass());
Expand Down
6 changes: 6 additions & 0 deletions src/bundle/Resources/config/ezplatform_default_settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ parameters:

# Registration
ibexa.site_access.config.default.user_registration.group_id: 11
ibexa.site_access.config.default.user_registration.user_type_identifier: 'user'
ibexa.site_access.config.default.user_registration.templates.form: "@@IbexaContentForms/Content/content_edit.html.twig"
ibexa.site_access.config.default.user_registration.templates.confirmation: "@@IbexaUser/register/register_confirmation.html.twig"

# Invitation
ibexa.site_access.config.default.user_invitation.templates.form: "@@IbexaUser/invitation/form.html.twig"
ibexa.site_access.config.default.user_invitation.templates.mail: "@@IbexaUser/invitation/mail/user_invitation.html.twig"
ibexa.site_access.config.default.user_invitation.hash_expiration_time: 'P2D'

# User Settings
ibexa.site_access.config.default.user_settings_update_view: {}
ibexa.site_access.config.default.user_settings_update_view_defaults:
Expand Down
6 changes: 6 additions & 0 deletions src/bundle/Resources/config/installer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
Ibexa\User\EventListener\BuildSchemaSubscriber:
autoconfigure: true
public: false
arguments:
- '@=service("kernel").locateResource("@IbexaUserBundle/Resources/config/storage/schema.yaml")'
15 changes: 15 additions & 0 deletions src/bundle/Resources/config/routing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ ibexa.user.user_register:
<<: *user.register
path: /user/register

ibexa.user.from_invite.register: &user.from_invite.register
path: /from-invite/register/{inviteHash}
defaults:
_controller: 'Ibexa\Bundle\User\Controller\UserRegisterController::registerFromInvitationAction'

ibexa.user.from_invite.user_register:
<<: *user.from_invite.register
path: /user/from-invite/register/{inviteHash}

ibexa.user.register_confirmation: &user.register_confirmation
path: /register-confirm
defaults:
Expand All @@ -46,6 +55,12 @@ ibexa.user.user_register_confirmation:
<<: *user.register_confirmation
path: /user/register-confirm

ibexa.user.invite_user:
path: /user/invite
methods: [POST, GET]
defaults:
_controller: 'Ibexa\Bundle\User\Controller\UserInvitationController::inviteUser'

ibexa.user_settings.list:
path: /user/settings/list/{page}
defaults:
Expand Down
6 changes: 3 additions & 3 deletions src/bundle/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ imports:
- { resource: services/user_settings.yaml }
- { resource: services/forms.yaml }
- { resource: services/profile_image.yaml }
- { resource: services/invitation.yaml }

parameters:
ibexa.user.content_type_identifier: user
Expand All @@ -16,6 +17,7 @@ services:

Ibexa\User\EventListener\:
resource: "../../../lib/EventListener/*"
exclude: '../../../lib/EventListener/{BuildSchemaSubscriber.php}'
public: true
tags:
- { name: kernel.event_subscriber }
Expand All @@ -35,9 +37,7 @@ services:

Ibexa\User\ConfigResolver\ConfigurableRegistrationGroupLoader: ~

Ibexa\User\ConfigResolver\ConfigurableRegistrationContentTypeLoader:
calls:
- [setParam, ["contentTypeIdentifier", '%ibexa.user.content_type_identifier%']]
Ibexa\User\ConfigResolver\ConfigurableRegistrationContentTypeLoader: ~

# Default implementations
Ibexa\User\ConfigResolver\RegistrationGroupLoader: '@Ibexa\User\ConfigResolver\ConfigurableRegistrationGroupLoader'
Expand Down
Loading

0 comments on commit fda7714

Please sign in to comment.