Skip to content

Commit

Permalink
IBX-3201: Added invitation email validation (#46)
Browse files Browse the repository at this point in the history
* IBX-3201: Added invitation email validation

* IBX-3201: Refreshing invitations now also changes hash of it

* IBX-3361: Increased hash expiration date to 7 days
  • Loading branch information
ViniTou authored Aug 2, 2022
1 parent 6799326 commit ebe1829
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ parameters:
# 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'
ibexa.site_access.config.default.user_invitation.hash_expiration_time: 'P7D'

# User Settings
ibexa.site_access.config.default.user_settings_update_view: {}
Expand Down
6 changes: 6 additions & 0 deletions src/bundle/Resources/config/services/validators.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ services:
$userService: '@ibexa.api.service.user'
tags:
- { name: validator.constraint_validator }

Ibexa\User\Validator\Constraints\EmailInvitationValidator:
arguments:
$userService: '@ibexa.api.service.user'
tags:
- { name: validator.constraint_validator }
5 changes: 5 additions & 0 deletions src/bundle/Resources/translations/validators.en.xliff
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<target state="new">Passwords do not match.</target>
<note>key: ezplatform.reset_user_password.passwords_must_match</note>
</trans-unit>
<trans-unit id="cfba8aa1f9a4e5948ec73f46c8461f6ce35aa856" resname="ibexa.user.invitation.user_with_email_exists">
<source>The email '%email%' is already in your member list.</source>
<target state="new">The email '%email%' is already in your member list.</target>
<note>key: ibexa.user.invitation.user_with_email_exists</note>
</trans-unit>
</body>
</file>
</xliff>
2 changes: 1 addition & 1 deletion src/contracts/Invitation/InvitationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ public function markAsUsed(Invitation $invitation): void;
*/
public function findInvitations(?InvitationFilter $invitationsFilter = null): array;

public function refreshInvitation(Invitation $invitation): void;
public function refreshInvitation(Invitation $invitation): Invitation;
}
2 changes: 1 addition & 1 deletion src/contracts/Invitation/Persistence/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ public function markAsUsed(string $hash): void;
/** @return \Ibexa\Contracts\User\Invitation\Persistence\Invitation[] */
public function findInvitations(?InvitationFilter $invitationsFilter = null): array;

public function refreshInvitation(string $hash): void;
public function refreshInvitation(string $hash, string $newHash): Invitation;
}
16 changes: 15 additions & 1 deletion src/contracts/Invitation/Persistence/InvitationUpdateStruct.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ final class InvitationUpdateStruct extends ValueObject

private ?bool $isUsed;

private ?string $hash;

public function __construct(
?int $createdAt = null,
?bool $isUsed = null
?bool $isUsed = null,
?string $hash = null
) {
parent::__construct();

$this->createdAt = $createdAt;
$this->isUsed = $isUsed;
$this->hash = $hash;
}

public function getCreatedAt(): ?int
Expand All @@ -45,4 +49,14 @@ public function setIsUsed(?bool $isUsed): void
{
$this->isUsed = $isUsed;
}

public function getHash(): ?string
{
return $this->hash;
}

public function setHash(?string $hash): void
{
$this->hash = $hash;
}
}
6 changes: 4 additions & 2 deletions src/lib/Invitation/InvitationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public function findInvitations(?InvitationFilter $invitationsFilter = null): ar
});
}

public function refreshInvitation(Invitation $invitation): void
public function refreshInvitation(Invitation $invitation): Invitation
{
if (!$this->permissionResolver->hasAccess('user', 'invite')) {
throw new UnauthorizedException('user', 'invite');
Expand All @@ -224,6 +224,8 @@ public function refreshInvitation(Invitation $invitation): void
throw new UnauthorizedException('user', 'invite', ['role' => $role->identifier]);
}

$this->handler->refreshInvitation($invitation->getHash());
return $this->domainMapper->buildDomainObject(
$this->handler->refreshInvitation($invitation->getHash(), $this->hashGenerator->generate())
);
}
}
6 changes: 5 additions & 1 deletion src/lib/Invitation/Persistence/DoctrineGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ public function updateInvitation(string $hash, InvitationUpdateStruct $updateStr
'value' => $updateStruct->getIsUsed(),
'type' => ParameterType::BOOLEAN,
],
'hash' => [
'value' => $updateStruct->getHash(),
'type' => ParameterType::STRING,
],
];

foreach ($fieldsForUpdateMap as $fieldName => $field) {
Expand All @@ -223,7 +227,7 @@ public function updateInvitation(string $hash, InvitationUpdateStruct $updateStr
$query->where(
$query->expr()->eq(
'hash',
$query->createNamedParameter($hash, ParameterType::STRING, ':hash')
$query->createNamedParameter($hash, ParameterType::STRING, ':current_hash')
)
);

Expand Down
9 changes: 7 additions & 2 deletions src/lib/Invitation/Persistence/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

class Handler implements HandlerInterface
{
private DoctrineGateway $gateway;
private Gateway $gateway;

private Mapper $mapper;

Expand Down Expand Up @@ -101,11 +101,16 @@ public function findInvitations(?InvitationFilter $invitationsFilter = null): ar
return $invitations;
}

public function refreshInvitation(string $hash): void
public function refreshInvitation(string $hash, string $newHash): Invitation
{
$updateStruct = new InvitationUpdateStruct();
$updateStruct->setCreatedAt(time());
$updateStruct->setHash($newHash);

$this->gateway->updateInvitation($hash, $updateStruct);

return $this->mapper->extractInvitationFromRow(
$this->gateway->getInvitation($newHash)
);
}
}
29 changes: 29 additions & 0 deletions src/lib/Validator/Constraints/EmailInvitation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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\User\Validator\Constraints;

use JMS\TranslationBundle\Model\Message;
use JMS\TranslationBundle\Translation\TranslationContainerInterface;
use Symfony\Component\Validator\Constraint;

/**
* @Annotation
*/
class EmailInvitation extends Constraint implements TranslationContainerInterface
{
public string $message = 'ibexa.user.invitation.user_with_email_exists';

public static function getTranslationMessages()
{
return [
Message::create('ibexa.user.invitation.user_with_email_exists', 'validators')
->setDesc("The email '%email%' is already in your member list."),
];
}
}
40 changes: 40 additions & 0 deletions src/lib/Validator/Constraints/EmailInvitationValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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\User\Validator\Constraints;

use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException;
use Ibexa\Contracts\Core\Repository\UserService;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class EmailInvitationValidator extends ConstraintValidator
{
private UserService $userService;

public function __construct(UserService $userService)
{
$this->userService = $userService;
}

/**
* Checks if the passed value is valid.
*
* @param string $email The value that should be validated
* @param \Symfony\Component\Validator\Constraint $constraint The constraint for the validation
*/
public function validate($email, Constraint $constraint)
{
try {
$this->userService->loadUserByEmail($email);
$this->context->addViolation($constraint->message, ['%email%' => $email]);
} catch (NotFoundException $exception) {
// Do nothing
}
}
}
5 changes: 3 additions & 2 deletions tests/integration/Invitation/InvitationServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,11 @@ public function testRefreshInvitation(): void
);
sleep(5);

$this->invitationService->refreshInvitation($invitation);
$refreshed = $this->invitationService->getInvitation($invitation->getHash());
$refreshed = $this->invitationService->refreshInvitation($invitation);

self::assertGreaterThan($invitation->createdAt()->getTimestamp(), $refreshed->createdAt()->getTimestamp());
self::assertNotEquals($invitation->getHash(), $refreshed->getHash());

ClockMock::withClockMock(false);
}
}

0 comments on commit ebe1829

Please sign in to comment.