diff --git a/app/AppKernel.php b/app/AppKernel.php
index d0205f575..4416fc7fa 100644
--- a/app/AppKernel.php
+++ b/app/AppKernel.php
@@ -67,6 +67,7 @@ public function registerBundles()
new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),
new Snc\RedisBundle\SncRedisBundle(),
new Http\HttplugBundle\HttplugBundle(),
+ new LoginCidadao\SupportBundle\LoginCidadaoSupportBundle(),
// PROCERGS
new PROCERGS\LoginCidadao\CoreBundle\PROCERGSLoginCidadaoCoreBundle(),
diff --git a/app/Resources/translations/messages.pt_BR.yml b/app/Resources/translations/messages.pt_BR.yml
index f4eaa9764..f966c6186 100644
--- a/app/Resources/translations/messages.pt_BR.yml
+++ b/app/Resources/translations/messages.pt_BR.yml
@@ -1,4 +1,6 @@
password_hint:
range:
- with_reqs: Senha de %min% ou mais %reqs%.
- no_reqs: Senha de %min% ou mais caracteres.
+ with_reqs: "Senha de %min% ou mais %reqs%."
+ no_reqs: "Senha de %min% ou mais caracteres."
+
+support.person.view.connections.nfg.label: "Nota Fiscal Gaúcha"
diff --git a/app/config/config.yml b/app/config/config.yml
index 09f085e69..4010e7db6 100644
--- a/app/config/config.yml
+++ b/app/config/config.yml
@@ -6,6 +6,7 @@ imports:
- { resource: logstash.yml }
- { resource: "@LoginCidadaoTOSBundle/Resources/config/config.yml" }
- { resource: 'workflows/*.{yml,yaml}' }
+ - { resource: "@LoginCidadaoSupportBundle/Resources/config/services.yml" }
framework:
#esi: ~
@@ -296,6 +297,7 @@ simple_things_entity_audit:
- LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerification
- LoginCidadao\PhoneVerificationBundle\Entity\SentVerification
+ - LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumber
- PROCERGS\LoginCidadao\CoreBunde\Entity\PersonMeuRS
- PROCERGS\LoginCidadao\NfgBunde\Entity\NfgProfile
@@ -605,6 +607,14 @@ services:
fos_user.doctrine_registry:
alias: doctrine
+ # Extend Third Party Connections in the Support Page
+ PROCERGS\LoginCidadao\CoreBundle\Service\SupportHandlerDecorator:
+ decorates: 'LoginCidadao\SupportBundle\Service\SupportHandler'
+ arguments:
+ - '@PROCERGS\LoginCidadao\CoreBundle\Service\SupportHandlerDecorator.inner'
+ - '@meurs.helper'
+ - '@lc.person.repository'
+
donato_path_well:
enabled: '%check_pathwell_topologies%'
diff --git a/app/config/routing.yml b/app/config/routing.yml
index d87cb53c5..343858e1b 100644
--- a/app/config/routing.yml
+++ b/app/config/routing.yml
@@ -7,6 +7,11 @@ procergs_nfg:
type: annotation
prefix: /nfg
+support:
+ resource: "@LoginCidadaoSupportBundle/Controller/"
+ type: annotation
+ prefix: /
+
# Remote Claims
lc_remote_claims_remote_claim:
type: rest
diff --git a/app/config/security.yml b/app/config/security.yml
index 4ffb7ddea..1b56b3593 100644
--- a/app/config/security.yml
+++ b/app/config/security.yml
@@ -36,6 +36,8 @@ security:
- ROLE_PERSON_EDIT
- ROLE_VIEW_3RD_PARTY_CONNECTIONS
- ROLE_EDIT_CLIENT_SUBJECT_TYPE
+ - ROLE_EDIT_BLOCKED_PHONES
+ - ROLE_SUPPORT_MANAGER
- ROLE_ACCOUNTANT
ROLE_SUPER_ADMIN:
- ROLE_ADMIN
@@ -52,6 +54,7 @@ security:
- ROLE_AUDIT_PROFILE_VIEWS
- ROLE_VIEW_3RD_PARTY_CONNECTIONS_USERNAMES
- ROLE_PERSON_BLOCK
+ - ROLE_SKIP_SUPPORT_TOKEN_VALIDATION
ROLE_ALLOWED_TO_SWITCH: FEATURE_IMPERSONATION_REPORTS
ROLE_EDIT_CLIENT_ALLOWED_RESTRICTED_SCOPES: ROLE_EDIT_CLIENT_ALLOWED_SCOPES
@@ -61,15 +64,33 @@ security:
ROLE_VIEW_3RD_PARTY_CONNECTIONS:
ROLE_PERSON_BLOCK:
ROLE_EDIT_CLIENT_SUBJECT_TYPE:
+ ROLE_EDIT_BLOCKED_PHONES:
+ ROLE_SKIP_SUPPORT_TOKEN_VALIDATION:
FEATURE_ALPHA: FEATURE_BETA, FEATURE_SHOW_PROFILE_VIEWS
FEATURE_BETA: FEATURE_PROD, FEATURE_ORGANIZATIONS
- FEATURE_PROD: FEATURE_2FACTOR_AUTH, FEATURE_INVALIDATE_SESSIONS, FEATURE_REMOTE_CLAIMS
+ FEATURE_PROD: FEATURE_2FACTOR_AUTH, FEATURE_INVALIDATE_SESSIONS, FEATURE_REMOTE_CLAIMS, FEATURE_PHONE_BLOCKLIST
FEATURE_IGP_VALIDATION:
FEATURE_ORGANIZATIONS:
FEATURE_IMPERSONATION_REPORTS:
FEATURE_EDIT_USERNAME:
FEATURE_SHOW_PROFILE_VIEWS:
FEATURE_REMOTE_CLAIMS:
+ FEATURE_PHONE_BLOCKLIST:
+
+ # Support Roles
+ ROLE_SUPPORT_AGENT:
+ - ROLE_SUPPORT_SEARCH_USERS
+ ROLE_SUPPORT_MANAGER:
+ - ROLE_SUPPORT_AGENT
+ - ROLE_SUPPORT_MANAGE_AGENTS
+ - ROLE_SUPPORT_VIEW_EMAIL
+ - ROLE_SUPPORT_VIEW_PHONE
+ - ROLE_SUPPORT_VIEW_BIRTHDAY
+ ROLE_SUPPORT_MANAGE_AGENTS:
+ ROLE_SUPPORT_SEARCH_USERS:
+ ROLE_SUPPORT_VIEW_EMAIL:
+ ROLE_SUPPORT_VIEW_PHONE:
+ ROLE_SUPPORT_VIEW_BIRTHDAY:
providers:
chainprovider:
@@ -164,7 +185,7 @@ security:
- { path: ^/api/v1/address/(cities|states|countries)/search, role: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
- { path: ^/api/v1/public/lc_consultaCep2, role: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
- { path: ^/api/v1/statistics, role: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
- - { path: ^/api/v1/phone-verification/(update-status|average-delivery-time|not-delivered), role: IS_AUTHENTICATED_ANONYMOUSLY, ip: %allowed_monitors%, requires_channel: https }
+ - { path: ^/api/v1/phone-verification/(update-status|average-delivery-time|not-delivered), role: IS_AUTHENTICATED_ANONYMOUSLY, ips: "%allowed_monitors%", requires_channel: https }
- { path: ^/api/v1/accounting, role: IS_AUTHENTICATED_ANONYMOUSLY, ip: %allowed_accounting%, requires_channel: https }
- { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ], requires_channel: https }
diff --git a/src/LoginCidadao/APIBundle/Controller/PersonController.php b/src/LoginCidadao/APIBundle/Controller/PersonController.php
index 1fd4e99eb..c5246ac5c 100644
--- a/src/LoginCidadao/APIBundle/Controller/PersonController.php
+++ b/src/LoginCidadao/APIBundle/Controller/PersonController.php
@@ -145,7 +145,7 @@ public function getLogoutKeyAction($id)
$em->persist($logoutKey);
$em->flush();
- $result = [
+ return [
'key' => $logoutKey->getKey(),
'url' => $this->generateUrl(
'lc_logout_not_remembered_safe',
@@ -153,7 +153,5 @@ public function getLogoutKeyAction($id)
UrlGeneratorInterface::ABSOLUTE_URL
),
];
-
- return $result;
}
}
diff --git a/src/LoginCidadao/APIBundle/Security/Audit/ActionLogger.php b/src/LoginCidadao/APIBundle/Security/Audit/ActionLogger.php
index 60f3cb6d8..08ac9d2a0 100644
--- a/src/LoginCidadao/APIBundle/Security/Audit/ActionLogger.php
+++ b/src/LoginCidadao/APIBundle/Security/Audit/ActionLogger.php
@@ -10,6 +10,7 @@
namespace LoginCidadao\APIBundle\Security\Audit;
+use LoginCidadao\CoreBundle\Model\IdentifiablePersonInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
@@ -188,14 +189,14 @@ public function registerImpersonate(
/**
* @param Request $request
- * @param PersonInterface $person
- * @param PersonInterface $viewer
+ * @param IdentifiablePersonInterface $person
+ * @param IdentifiablePersonInterface $viewer
* @param array $controllerAction
*/
public function registerProfileView(
Request $request,
- PersonInterface $person,
- PersonInterface $viewer,
+ IdentifiablePersonInterface $person,
+ IdentifiablePersonInterface $viewer,
array $controllerAction
) {
$auditUsername = $this->auditConfig->getCurrentUsername();
@@ -206,8 +207,8 @@ public function registerProfileView(
private function registerActionLog(
Request $request,
- PersonInterface $person,
- PersonInterface $actor,
+ IdentifiablePersonInterface $person,
+ IdentifiablePersonInterface $actor,
array $controllerAction,
$auditUsername,
$actionType
diff --git a/src/LoginCidadao/CoreBundle/Controller/Admin/PersonController.php b/src/LoginCidadao/CoreBundle/Controller/Admin/PersonController.php
index 8143dbe48..8b018ddfd 100644
--- a/src/LoginCidadao/CoreBundle/Controller/Admin/PersonController.php
+++ b/src/LoginCidadao/CoreBundle/Controller/Admin/PersonController.php
@@ -1,4 +1,12 @@
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
namespace LoginCidadao\CoreBundle\Controller\Admin;
@@ -6,6 +14,7 @@
use libphonenumber\PhoneNumber;
use LoginCidadao\APIBundle\Security\Audit\ActionLogger;
use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Form\Type\PersonFilterFormType;
use LoginCidadao\CoreBundle\Form\Type\PersonResumeFormType;
use LoginCidadao\CoreBundle\Security\User\Manager\UserManager;
use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationServiceInterface;
@@ -39,7 +48,7 @@ public function indexAction(Request $request)
if ($request->get('search') !== null) {
$data = ['username' => $request->get('search')];
}
- $form = $this->createForm('LoginCidadao\CoreBundle\Form\Type\PersonFilterFormType', $data);
+ $form = $this->createForm(PersonFilterFormType::class, $data);
$form = $form->createView();
return compact('form');
@@ -114,7 +123,7 @@ public function blockAction(Request $request, $id, $token)
*/
public function gridAction(Request $request)
{
- $form = $this->createForm('LoginCidadao\CoreBundle\Form\Type\PersonFilterFormType');
+ $form = $this->createForm(PersonFilterFormType::class);
$form->handleRequest($request);
$gridView = null;
if ($form->isValid()) {
diff --git a/src/LoginCidadao/CoreBundle/Controller/DefaultController.php b/src/LoginCidadao/CoreBundle/Controller/DefaultController.php
index 86094bd24..f23959ef2 100644
--- a/src/LoginCidadao/CoreBundle/Controller/DefaultController.php
+++ b/src/LoginCidadao/CoreBundle/Controller/DefaultController.php
@@ -92,19 +92,20 @@ public function dashboardAction()
// logs
$em = $this->getDoctrine()->getManager();
+ $showProfileViews = $this->isGranted('FEATURE_SHOW_PROFILE_VIEWS');
/** @var ActionLogRepository $logRepo */
$logRepo = $em->getRepository('LoginCidadaoAPIBundle:ActionLog');
$logs['logins'] = $logRepo->findLoginsByPerson($this->getUser(), 5);
- $logs['activity'] = $logRepo->getActivityLogsByTarget($this->getUser(), 4);
+ $logs['activity'] = $logRepo->getActivityLogsByTarget($this->getUser(), 4, $showProfileViews);
$defaultClientUid = $this->container->getParameter('oauth_default_client.uid');
- return array(
+ return [
'allBadges' => $badges,
'userBadges' => $userBadges,
'logs' => $logs,
'defaultClientUid' => $defaultClientUid,
- );
+ ];
}
/**
@@ -170,9 +171,10 @@ private function getEmail(SupportMessage $supportMessage, TranslatorInterface $t
{
$message = $supportMessage->getFormattedMessage($translator);
- $email = (new SentEmail())
+ $email = new SentEmail();
+ $email
->setType('contact-mail')
- ->setSubject('Fale conosco - '.$supportMessage->getName())
+ ->setSubject('Fale conosco - '.$supportMessage->getName()." - Ticket: {$email->getSupportTicket()}")
->setSender($supportMessage->getEmail())
->setReceiver($this->container->getParameter('contact_form.email'))
->setMessage($message);
diff --git a/src/LoginCidadao/CoreBundle/Controller/TaskController.php b/src/LoginCidadao/CoreBundle/Controller/TaskController.php
index 1575539d6..4ed2356ae 100644
--- a/src/LoginCidadao/CoreBundle/Controller/TaskController.php
+++ b/src/LoginCidadao/CoreBundle/Controller/TaskController.php
@@ -8,6 +8,7 @@
use FOS\UserBundle\Util\TokenGenerator;
use FOS\UserBundle\Event\GetResponseUserEvent;
use LoginCidadao\CoreBundle\Entity\Person;
+use LoginCidadao\CoreBundle\Form\Type\EmailFormType;
use LoginCidadao\CoreBundle\Model\ConfirmEmailTask;
use LoginCidadao\CoreBundle\Model\PersonInterface;
use LoginCidadao\TaskStackBundle\Model\RouteTaskTarget;
@@ -57,7 +58,7 @@ public function confirmEmailAction(Request $request)
$event = new GetResponseUserEvent($person, $request);
$dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_INITIALIZE, $event);
- $form = $this->createForm('LoginCidadao\CoreBundle\Form\Type\EmailFormType', $person);
+ $form = $this->createForm(EmailFormType::class, $person);
$response = null;
$form->handleRequest($request);
diff --git a/src/LoginCidadao/CoreBundle/Entity/PersonRepository.php b/src/LoginCidadao/CoreBundle/Entity/PersonRepository.php
index 6f2c7de9c..a62161c5a 100644
--- a/src/LoginCidadao/CoreBundle/Entity/PersonRepository.php
+++ b/src/LoginCidadao/CoreBundle/Entity/PersonRepository.php
@@ -18,6 +18,7 @@
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
+use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
class PersonRepository extends EntityRepository
{
@@ -222,6 +223,20 @@ public function getNameSearchQuery($name)
->setParameter('name', "%{$sanitized}%");
}
+ public function getPhoneSearchQuery(PhoneNumber $phoneNumber)
+ {
+ return $this->getBaseSearchQuery()
+ ->where('p.mobile = :mobile')
+ ->setParameter('mobile', $phoneNumber, PhoneNumberType::NAME);
+ }
+
+ public function getPartialPhoneSearchQuery(string $phoneNumber)
+ {
+ return $this->getBaseSearchQuery()
+ ->where('p.mobile LIKE :mobile')
+ ->setParameter('mobile', "{$phoneNumber}%");
+ }
+
/**
* This will return the appropriate query for the input given.
* @param $query
@@ -249,6 +264,10 @@ public function getSmartSearchQuery($query)
return $this->getEmailSearchQuery($query);
}
+ if ($query[0] === '+') {
+ return $this->getPartialPhoneSearchQuery($query);
+ }
+
// Defaults to name search
return $this->getNameSearchQuery($query);
}
diff --git a/src/LoginCidadao/CoreBundle/Entity/SentEmail.php b/src/LoginCidadao/CoreBundle/Entity/SentEmail.php
index ca622f893..e7faeaae9 100644
--- a/src/LoginCidadao/CoreBundle/Entity/SentEmail.php
+++ b/src/LoginCidadao/CoreBundle/Entity/SentEmail.php
@@ -11,6 +11,7 @@
namespace LoginCidadao\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
+use Ramsey\Uuid\Uuid;
/**
* SentEmail
@@ -71,9 +72,21 @@ class SentEmail
*/
private $type;
+ /**
+ * @var string
+ *
+ * @ORM\Column(name="support_ticket", type="string", length=255, unique=true)
+ */
+ private $supportTicket;
+
public function __construct()
{
$this->setDate(new \DateTime());
+ try {
+ $this->supportTicket = Uuid::uuid4();
+ } catch (\Exception $e) {
+ $this->supportTicket = bin2hex(random_bytes(4));
+ }
}
/**
@@ -224,6 +237,14 @@ public function getType()
return $this->type;
}
+ /**
+ * @return string
+ */
+ public function getSupportTicket(): string
+ {
+ return $this->supportTicket;
+ }
+
public function getSwiftMail()
{
return (new \Swift_Message($this->getSubject()))
diff --git a/src/LoginCidadao/CoreBundle/EventListener/RequestListener.php b/src/LoginCidadao/CoreBundle/EventListener/RequestListener.php
index 5723ed838..68e73e6bd 100644
--- a/src/LoginCidadao/CoreBundle/EventListener/RequestListener.php
+++ b/src/LoginCidadao/CoreBundle/EventListener/RequestListener.php
@@ -10,39 +10,62 @@
namespace LoginCidadao\CoreBundle\EventListener;
+use FOS\UserBundle\Model\FosUserInterface;
use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
-use Symfony\Component\HttpKernel\HttpKernel;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class RequestListener
{
/** @var LoggerInterface */
private $logger;
+ /** @var TokenStorageInterface */
+ private $tokenStorage;
+
+ /** @var RouterInterface */
+ private $router;
+
/**
* RequestListener constructor.
* @param LoggerInterface $logger
+ * @param TokenStorageInterface $tokenStorage
+ * @param RouterInterface $router
*/
- public function __construct(LoggerInterface $logger)
+ public function __construct(LoggerInterface $logger, TokenStorageInterface $tokenStorage, RouterInterface $router)
{
$this->logger = $logger;
+ $this->tokenStorage = $tokenStorage;
+ $this->router = $router;
}
public function onKernelRequest(GetResponseEvent $event)
{
- if (HttpKernel::MASTER_REQUEST === $event->getRequestType()) {
+ if ($event->isMasterRequest()) {
$this->logReferer($event);
+ $this->checkUserEnabled($event);
}
}
private function logReferer(GetResponseEvent $event)
{
- /** @var string|false $referer */
- $referer = $event->getRequest()->headers->get('referer', false);
- if (false === $referer) {
- return;
+ $referer = $event->getRequest()->headers->get('referer', null);
+ if (null !== $referer) {
+ $this->logger->info("Request referrer: {$referer}");
}
+ }
- $this->logger->info("Request referrer: {$referer}");
+ private function checkUserEnabled(GetResponseEvent $event)
+ {
+ if (null !== $token = $this->tokenStorage->getToken()) {
+ /** @var FosUserInterface $person */
+ if (($person = $token->getUser()) instanceof FosUserInterface && false === $person->isEnabled()) {
+ $uri = $this->router->generate('fos_user_security_logout');
+ $event->setResponse(new RedirectResponse($uri));
+ $event->stopPropagation();
+ }
+ }
}
}
diff --git a/src/LoginCidadao/CoreBundle/Form/Type/PersonFilterFormType.php b/src/LoginCidadao/CoreBundle/Form/Type/PersonFilterFormType.php
index fb9ae660c..ed0e01088 100644
--- a/src/LoginCidadao/CoreBundle/Form/Type/PersonFilterFormType.php
+++ b/src/LoginCidadao/CoreBundle/Form/Type/PersonFilterFormType.php
@@ -1,4 +1,12 @@
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
namespace LoginCidadao\CoreBundle\Form\Type;
diff --git a/src/LoginCidadao/CoreBundle/Form/Type/ProfileFormType.php b/src/LoginCidadao/CoreBundle/Form/Type/ProfileFormType.php
index 2a6aa382f..1a6de27bd 100644
--- a/src/LoginCidadao/CoreBundle/Form/Type/ProfileFormType.php
+++ b/src/LoginCidadao/CoreBundle/Form/Type/ProfileFormType.php
@@ -12,10 +12,17 @@
use Doctrine\ORM\EntityManagerInterface;
use libphonenumber\PhoneNumberFormat;
+use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
+use Symfony\Bridge\Doctrine\Form\Type\EntityType;
+use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use FOS\UserBundle\Form\Type\ProfileFormType as BaseType;
use Doctrine\ORM\EntityRepository;
use LoginCidadao\CoreBundle\Entity\Country;
+use Vich\UploaderBundle\Form\Type\VichFileType;
class ProfileFormType extends BaseType
{
@@ -41,68 +48,59 @@ public function buildForm(FormBuilderInterface $builder, array $options)
'iso2' => $this->defaultCountryIso2,
));
- $emailType = 'Symfony\Component\Form\Extension\Core\Type\EmailType';
- $textType = 'Symfony\Component\Form\Extension\Core\Type\TextType';
+ $emailType = EmailType::class;
+ $textType = TextType::class;
$builder
->add('email', $emailType, ['label' => 'form.email', 'translation_domain' => 'FOSUserBundle'])
->add('firstName', $textType, ['label' => 'form.firstName', 'translation_domain' => 'FOSUserBundle'])
->add('surname', $textType, ['label' => 'form.surname', 'translation_domain' => 'FOSUserBundle'])
- ->add('birthdate',
- 'Symfony\Component\Form\Extension\Core\Type\BirthdayType',
- [
- 'required' => false,
- 'format' => 'dd/MM/yyyy',
- 'widget' => 'single_text',
- 'label' => 'form.birthdate',
- 'translation_domain' => 'FOSUserBundle',
- 'attr' => ['pattern' => '[0-9/]*', 'class' => 'form-control birthdate'],
- ])
- ->add('mobile', 'Misd\PhoneNumberBundle\Form\Type\PhoneNumberType',
- [
- 'required' => false,
- 'label' => 'person.form.mobile.label',
- 'attr' => [
- 'class' => 'form-control intl-tel',
- 'placeholder' => 'person.form.mobile.placeholder',
- ],
- 'label_attr' => ['class' => 'intl-tel-label'],
- 'format' => PhoneNumberFormat::E164,
- ])
- ->add('image', 'Vich\UploaderBundle\Form\Type\VichFileType',
- [
- 'required' => false,
- 'allow_delete' => true, // not mandatory, default is true
- 'download_uri' => true,// not mandatory, default is true
- ])
- ->add('placeOfBirth',
- 'LoginCidadao\CoreBundle\Form\Type\CitySelectorComboType',
- [
- 'required' => false,
- 'level' => 'city',
- 'city_label' => 'Place of birth - City',
- 'state_label' => 'Place of birth - State',
- 'country_label' => 'Place of birth - Country',
- ]);
- $builder->add('nationality',
- 'Symfony\Bridge\Doctrine\Form\Type\EntityType',
- [
+ ->add('birthdate', BirthdayType::class, [
'required' => false,
- 'class' => 'LoginCidadaoCoreBundle:Country',
- 'choice_label' => 'name',
- 'preferred_choices' => [$country],
- 'choice_translation_domain' => true,
- 'query_builder' => function (EntityRepository $er) {
- return $er->createQueryBuilder('u')
- ->where('u.reviewed = :reviewed')
- ->setParameter('reviewed', Country::REVIEWED_OK)
- ->orderBy('u.name', 'ASC');
- },
- 'label' => 'Nationality',
+ 'format' => 'dd/MM/yyyy',
+ 'widget' => 'single_text',
+ 'label' => 'form.birthdate',
+ 'translation_domain' => 'FOSUserBundle',
+ 'attr' => ['pattern' => '[0-9/]*', 'class' => 'form-control birthdate'],
+ ])
+ ->add('mobile', PhoneNumberType::class, [
+ 'required' => false,
+ 'label' => 'person.form.mobile.label',
+ 'attr' => [
+ 'class' => 'form-control intl-tel',
+ 'placeholder' => 'person.form.mobile.placeholder',
+ ],
+ 'label_attr' => ['class' => 'intl-tel-label'],
+ 'format' => PhoneNumberFormat::E164,
+ ])
+ ->add('image', VichFileType::class, [
+ 'required' => false,
+ 'allow_delete' => true, // not mandatory, default is true
+ 'download_uri' => true,// not mandatory, default is true
+ ])
+ ->add('placeOfBirth', CitySelectorComboType::class, [
+ 'required' => false,
+ 'level' => 'city',
+ 'city_label' => 'Place of birth - City',
+ 'state_label' => 'Place of birth - State',
+ 'country_label' => 'Place of birth - Country',
]);
+ $builder->add('nationality', EntityType::class, [
+ 'required' => false,
+ 'class' => 'LoginCidadaoCoreBundle:Country',
+ 'choice_label' => 'name',
+ 'preferred_choices' => [$country],
+ 'choice_translation_domain' => true,
+ 'query_builder' => function (EntityRepository $er) {
+ return $er->createQueryBuilder('u')
+ ->where('u.reviewed = :reviewed')
+ ->setParameter('reviewed', Country::REVIEWED_OK)
+ ->orderBy('u.name', 'ASC');
+ },
+ 'label' => 'Nationality',
+ ]);
- $builder->add('defaultCountry',
- 'Symfony\Component\Form\Extension\Core\Type\HiddenType',
+ $builder->add('defaultCountry', HiddenType::class,
["data" => $country->getId(), "required" => false, "mapped" => false]
);
}
diff --git a/src/LoginCidadao/CoreBundle/Mailer/TwigSwiftMailer.php b/src/LoginCidadao/CoreBundle/Mailer/TwigSwiftMailer.php
index acb9e1817..431355f41 100644
--- a/src/LoginCidadao/CoreBundle/Mailer/TwigSwiftMailer.php
+++ b/src/LoginCidadao/CoreBundle/Mailer/TwigSwiftMailer.php
@@ -48,4 +48,15 @@ public function sendAccountBlockedMessage(PersonInterface $person)
$context = ['email' => $person->getEmail()];
$this->sendMessage($template, $context, $from, $person->getEmail());
}
+
+ public function sendAccountAutoBlockedMessage(PersonInterface $person)
+ {
+ $template = $this->parameters['template']['account_auto_blocked'];
+ $fromEmail = $this->parameters['from_email']['account_auto_blocked'];
+ $fromName = $this->parameters['from_email']['email_sender_name'];
+ $from = [$fromEmail => $fromName];
+
+ $context = ['email' => $person->getEmail()];
+ $this->sendMessage($template, $context, $from, $person->getEmail());
+ }
}
diff --git a/src/LoginCidadao/CoreBundle/Model/IdentifiablePersonInterface.php b/src/LoginCidadao/CoreBundle/Model/IdentifiablePersonInterface.php
new file mode 100644
index 000000000..06d71a9d5
--- /dev/null
+++ b/src/LoginCidadao/CoreBundle/Model/IdentifiablePersonInterface.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\CoreBundle\Model;
+
+interface IdentifiablePersonInterface
+{
+ public function getId();
+}
diff --git a/src/LoginCidadao/CoreBundle/Model/PersonInterface.php b/src/LoginCidadao/CoreBundle/Model/PersonInterface.php
index 20f7b5efe..f8f79087c 100644
--- a/src/LoginCidadao/CoreBundle/Model/PersonInterface.php
+++ b/src/LoginCidadao/CoreBundle/Model/PersonInterface.php
@@ -24,10 +24,8 @@
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;
use JMS\Serializer\Annotation as JMS;
-interface PersonInterface extends EncoderAwareInterface, UserInterface, LocationAwareInterface, LongPollableInterface, TwoFactorInterface
+interface PersonInterface extends IdentifiablePersonInterface, EncoderAwareInterface, UserInterface, LocationAwareInterface, LongPollableInterface, TwoFactorInterface
{
- public function getId();
-
public function getEmail();
/**
@@ -294,4 +292,11 @@ public function getBackupCodes();
* @return PersonInterface
*/
public function setUpdatedAt($updatedAt = null);
+
+ /**
+ * @return \DateTimeInterface|null
+ */
+ public function getPasswordRequestedAt();
+
+ public function getLastLogin();
}
diff --git a/src/LoginCidadao/CoreBundle/Resources/config/services.yml b/src/LoginCidadao/CoreBundle/Resources/config/services.yml
index 9c4c05ac1..e1633e588 100644
--- a/src/LoginCidadao/CoreBundle/Resources/config/services.yml
+++ b/src/LoginCidadao/CoreBundle/Resources/config/services.yml
@@ -190,11 +190,13 @@ services:
email_changed: "%lc.emailChanged.template%"
notification_email: "%lc.notification.email.template%"
account_blocked: "LoginCidadaoCoreBundle:Person:accountBlocked.html.twig"
+ account_auto_blocked: "LoginCidadaoCoreBundle:Person:accountAutoBlocked.html.twig"
from_email:
confirmation: "%fos_user.registration.confirmation.from_email%"
resetting: "%fos_user.resetting.email.from_email%"
email_changed: "%mailer_sender_mail%"
account_blocked: "%mailer_sender_mail%"
+ account_auto_blocked: "%mailer_sender_mail%"
email_sender_name: "%mailer_sender_name%"
kernel.listener.logged_in_user_listener:
@@ -441,6 +443,8 @@ services:
class: LoginCidadao\CoreBundle\EventListener\RequestListener
arguments:
- "@logger"
+ - "@security.token_storage"
+ - "@router"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
- { name: monolog.logger, channel: request }
@@ -504,3 +508,9 @@ services:
- "@?snc_redis.default"
tags:
- { name: liip_monitor.check, alias: redis_predis }
+
+ lc.sent_email.repository:
+ class: Doctrine\ORM\EntityRepository
+ factory: ["@doctrine.orm.entity_manager", getRepository]
+ arguments:
+ - LoginCidadao\CoreBundle\Entity\SentEmail
diff --git a/src/LoginCidadao/CoreBundle/Resources/translations/messages.pt_BR.yml b/src/LoginCidadao/CoreBundle/Resources/translations/messages.pt_BR.yml
index 9e55c9dc6..b8e44c274 100644
--- a/src/LoginCidadao/CoreBundle/Resources/translations/messages.pt_BR.yml
+++ b/src/LoginCidadao/CoreBundle/Resources/translations/messages.pt_BR.yml
@@ -1031,3 +1031,36 @@ email:
Cordialmente,
Equipe Login Cidadão
+ account_auto_blocked:
+ message:
+ subject: 'Notificação de Bloqueio por Violação dos Termos de Uso'
+ text: |
+ Notificação de Bloqueio por Violação dos Termos de Uso:
+
+ Um mecanismo automático anti-fraude identificou uma atividade suspeita em sua conta do Login Cidadão registrada sob seu email %email%.
+
+ Foi identificada que a conta em questão poderia estar sendo utilizada para tentar acessar informações pessoais de terceiros, o que viola os Termos de Uso do Login Cidadão e, por esse motivo, foi bloqueada com base no texto:
+
+ "O Login Cidadão somente pode ser utilizado para fins pessoais, não sendo permitida a utilização em nome de terceiros ou com a finalidade de obter informações de terceiros."
+ (...)
+ "A não observância pelo usuário dos seus deveres previstos nestes Termos de Uso ou em qualquer outra legislação aplicável, poderá acarretar a suspensão ou cancelamento do seu acesso ao Login Cidadão, ou a um ou mais serviços disponibilizados, além de outras sanções previstas em lei."
+
+ Caso acredite que essa ação tenha sido executada por engano, é possível solicitar uma reavaliação não-automática através do forumlário de contato disponível no Login Cidadão.
+
+ Cordialmente,
+ Equipe Login Cidadão
+ html: >
+ Notificação de Bloqueio por Violação dos Termos de Uso:
+
+ Um mecanismo automático anti-fraude identificou uma atividade suspeita em sua conta do Login Cidadão registrada sob seu email %email%.
+
+ Foi identificada que a conta em questão poderia estar sendo utilizada para tentar acessar informações pessoais de terceiros, o que viola os Termos de Uso do Login Cidadão e, por esse motivo, foi bloqueada com base no texto:
+
+ "O Login Cidadão somente pode ser utilizado para fins pessoais, não sendo permitida a utilização em nome de terceiros ou com a finalidade de obter informações de terceiros."
+ (...)
+ "A não observância pelo usuário dos seus deveres previstos nestes Termos de Uso ou em qualquer outra legislação aplicável, poderá acarretar a suspensão ou cancelamento do seu acesso ao Login Cidadão, ou a um ou mais serviços disponibilizados, além de outras sanções previstas em lei."
+
+ Caso acredite que essa ação tenha sido executada por engano, é possível solicitar uma reavaliação não-automática através do forumlário de contato disponível no Login Cidadão.
+
+ Cordialmente,
+ Equipe Login Cidadão
diff --git a/src/LoginCidadao/CoreBundle/Resources/views/Admin/Client/index.html.twig b/src/LoginCidadao/CoreBundle/Resources/views/Admin/Client/index.html.twig
index e2929dafa..63f441a48 100644
--- a/src/LoginCidadao/CoreBundle/Resources/views/Admin/Client/index.html.twig
+++ b/src/LoginCidadao/CoreBundle/Resources/views/Admin/Client/index.html.twig
@@ -17,9 +17,3 @@
{% endblock content %}
-
-{% block javascripts %}
-{{ parent() }}
-
-{% endblock javascripts %}
diff --git a/src/LoginCidadao/CoreBundle/Resources/views/Admin/Person/grid.html.twig b/src/LoginCidadao/CoreBundle/Resources/views/Admin/Person/grid.html.twig
index d293a07d6..b926e7788 100644
--- a/src/LoginCidadao/CoreBundle/Resources/views/Admin/Person/grid.html.twig
+++ b/src/LoginCidadao/CoreBundle/Resources/views/Admin/Person/grid.html.twig
@@ -46,7 +46,17 @@
{{ 'cpf'|trans }}
- {{ row.cpf }}
+
+ {% if row.cpf is not null %}
+ {% if is_granted('ROLE_VIEW_USERS_CPF') %}
+ {{ row.cpf }}
+ {% else %}
+ {{ 'lc.admin.person.metadata.hasCpf.yes' | trans }}
+ {% endif %}
+ {% else %}
+ {{ 'lc.admin.person.metadata.hasCpf.no' | trans }}
+ {% endif %}
+
diff --git a/src/LoginCidadao/CoreBundle/Security/User/Manager/UserManager.php b/src/LoginCidadao/CoreBundle/Security/User/Manager/UserManager.php
index dd1d877c3..3c0a69552 100644
--- a/src/LoginCidadao/CoreBundle/Security/User/Manager/UserManager.php
+++ b/src/LoginCidadao/CoreBundle/Security/User/Manager/UserManager.php
@@ -18,6 +18,9 @@
class UserManager extends BaseManager
{
+ const FLUSH_STRATEGY_ONCE = 'once';
+ const FLUSH_STRATEGY_EACH = 'each';
+
public function createUser()
{
return parent::createUser();
@@ -122,8 +125,8 @@ public function blockPerson(PersonInterface $person, $andFlush = true)
public function blockUsersByPhone(PhoneNumber $phone, $flushStrategy = null)
{
- $andFlush = $flushStrategy === 'each' ? true : false;
- $once = $flushStrategy === 'once' ? true : false;
+ $andFlush = $flushStrategy === self::FLUSH_STRATEGY_EACH ? true : false;
+ $once = $flushStrategy === self::FLUSH_STRATEGY_ONCE ? true : false;
/** @var PersonInterface[] $users */
$users = parent::getRepository()->findBy(['mobile' => $phone]);
@@ -134,7 +137,6 @@ public function blockUsersByPhone(PhoneNumber $phone, $flushStrategy = null)
if ($user instanceof PersonInterface) {
$blockedUsers[] = $user;
}
- // TODO: add yield when we upgrade to PHP 7.0
}
if ($once) {
$this->objectManager->flush();
diff --git a/src/LoginCidadao/CoreBundle/Tests/EventListener/RequestListenerTest.php b/src/LoginCidadao/CoreBundle/Tests/EventListener/RequestListenerTest.php
index 6e1655326..98d9a09ff 100644
--- a/src/LoginCidadao/CoreBundle/Tests/EventListener/RequestListenerTest.php
+++ b/src/LoginCidadao/CoreBundle/Tests/EventListener/RequestListenerTest.php
@@ -10,69 +10,145 @@
namespace LoginCidadao\CoreBundle\Tests\EventListener;
+use FOS\UserBundle\Model\FosUserInterface;
use LoginCidadao\CoreBundle\EventListener\RequestListener;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
-use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class RequestListenerTest extends TestCase
{
public function testOnKernelRequest()
{
- /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject $logger */
- $logger = $this->createMock('Psr\Log\LoggerInterface');
- $logger->expects($this->once())
- ->method('info')->with($this->stringContains('https://example.com'));
+ $logger = $this->getLogger('https://example.com');
$request = new Request([], [], [], [], [], ['HTTP_REFERER' => 'https://example.com']);
- /** @var GetResponseEvent|\PHPUnit_Framework_MockObject_MockObject $event */
- $event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
- ->disableOriginalConstructor()->getMock();
- $event->expects($this->once())
- ->method('getRequestType')->willReturn(HttpKernelInterface::MASTER_REQUEST);
- $event->expects($this->once())
- ->method('getRequest')->willReturn($request);
+ $event = $this->getEvent(true, $request);
- $listener = new RequestListener($logger);
+ $listener = new RequestListener($logger, $this->getTokenStorage(), $this->getRouter());
$listener->onKernelRequest($event);
}
public function testNoReferer()
{
- /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject $logger */
- $logger = $this->createMock('Psr\Log\LoggerInterface');
- $logger->expects($this->never())->method('info');
+ $event = $this->getEvent(true, new Request());
- $request = new Request();
+ $listener = new RequestListener($this->getLogger(), $this->getTokenStorage(), $this->getRouter());
+ $listener->onKernelRequest($event);
+ }
- /** @var GetResponseEvent|\PHPUnit_Framework_MockObject_MockObject $event */
- $event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
- ->disableOriginalConstructor()->getMock();
- $event->expects($this->once())
- ->method('getRequestType')->willReturn(HttpKernelInterface::MASTER_REQUEST);
- $event->expects($this->once())
- ->method('getRequest')->willReturn($request);
+ public function testNotMasterRequest()
+ {
+ $event = $this->getEvent(false);
- $listener = new RequestListener($logger);
+ $listener = new RequestListener($this->getLogger(), $this->getTokenStorage(), $this->getRouter());
$listener->onKernelRequest($event);
}
- public function testNotMasterRequest()
+ public function testDisabledUser()
+ {
+ $user = $this->createMock(FosUserInterface::class);
+ $user->expects($this->once())->method('isEnabled')->willReturn(false);
+
+ $token = $this->createMock(TokenInterface::class);
+ $token->expects($this->once())->method('getUser')->willReturn($user);
+
+ $event = $this->getEvent(true);
+ $event->expects($this->once())->method('stopPropagation');
+ $event->expects($this->once())->method('setResponse')
+ ->with($this->isInstanceOf(RedirectResponse::class));
+
+ $tokenStorage = $this->getTokenStorage();
+ $tokenStorage->expects($this->once())->method('getToken')->willReturn($token);
+
+ $router = $this->getRouter('fos_user_security_logout');
+
+ $listener = new RequestListener($this->getLogger(), $tokenStorage, $router);
+ $listener->onKernelRequest($event);
+ }
+
+ public function testLoggedInButNoUser()
{
- /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject $logger */
- $logger = $this->createMock('Psr\Log\LoggerInterface');
- $logger->expects($this->never())->method('info');
+ $token = $this->createMock(TokenInterface::class);
+ $token->expects($this->once())->method('getUser')->willReturn(null);
+
+ $event = $this->getEvent(true);
+ $event->expects($this->never())->method('stopPropagation');
+ $event->expects($this->never())->method('setResponse');
+
+ $tokenStorage = $this->getTokenStorage();
+ $tokenStorage->expects($this->once())->method('getToken')->willReturn($token);
- /** @var GetResponseEvent|\PHPUnit_Framework_MockObject_MockObject $event */
- $event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
- ->disableOriginalConstructor()->getMock();
- $event->expects($this->once())
- ->method('getRequestType')->willReturn(HttpKernelInterface::SUB_REQUEST);
+ $router = $this->getRouter();
+ $router->expects($this->never())->method('generate');
- $listener = new RequestListener($logger);
+ $listener = new RequestListener($this->getLogger(), $tokenStorage, $router);
$listener->onKernelRequest($event);
}
+
+ /**
+ * @return MockObject|TokenStorageInterface
+ */
+ private function getTokenStorage()
+ {
+ return $this->createMock(TokenStorageInterface::class);
+ }
+
+ /**
+ * @param string|null $expected
+ * @return MockObject|RouterInterface
+ */
+ private function getRouter(string $expected = null)
+ {
+ $router = $this->createMock(RouterInterface::class);
+ if (null !== $expected) {
+ $router->expects($this->once())->method('generate')->with($expected)->willReturn($expected);
+ }
+
+ return $router;
+ }
+
+ /**
+ * @param bool $isMasterRequest
+ * @param Request|null $request
+ * @return MockObject|GetResponseEvent
+ */
+ private function getEvent(bool $isMasterRequest, Request $request = null)
+ {
+ /** @var GetResponseEvent|MockObject $event */
+ $event = $this->createMock(GetResponseEvent::class);
+ $event->expects($this->once())->method('isMasterRequest')->willReturn($isMasterRequest);
+
+ if (null === $request) {
+ $request = new Request();
+ $event->expects($this->any())->method('getRequest')->willReturn($request);
+ } else {
+ $event->expects($this->once())->method('getRequest')->willReturn($request);
+ }
+
+ return $event;
+ }
+
+ /**
+ * @param string|null $expectedString
+ * @return MockObject|LoggerInterface
+ */
+ private function getLogger(string $expectedString = null)
+ {
+ $logger = $this->createMock(LoggerInterface::class);
+ if (null !== $expectedString) {
+ $logger->expects($this->once())->method('info')->with($this->stringContains($expectedString));
+ } else {
+ $logger->expects($this->never())->method('info');
+ }
+
+ return $logger;
+ }
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Controller/AdminBlocklistController.php b/src/LoginCidadao/PhoneVerificationBundle/Controller/AdminBlocklistController.php
new file mode 100644
index 000000000..04092d965
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Controller/AdminBlocklistController.php
@@ -0,0 +1,180 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Controller;
+
+use libphonenumber\NumberParseException;
+use libphonenumber\PhoneNumber;
+use libphonenumber\PhoneNumberType;
+use libphonenumber\PhoneNumberUtil;
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Helper\GridHelper;
+use LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumber;
+use LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumberRepository;
+use LoginCidadao\PhoneVerificationBundle\Form\BlockPhoneFormType;
+use LoginCidadao\PhoneVerificationBundle\Form\SearchPhoneNumberType;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockedPhoneNumberInterface;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockPhoneNumberRequest;
+use LoginCidadao\PhoneVerificationBundle\Service\BlocklistInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Session\Session;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Routing\Annotation\Route;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
+
+/**
+ * Class AdminBlocklistController
+ * @package LoginCidadao\PhoneVerificationBundle\Controller
+ *
+ * @Security("has_role('ROLE_EDIT_BLOCKED_PHONES')")
+ * @codeCoverageIgnore
+ */
+class AdminBlocklistController extends Controller
+{
+ /**
+ * @Route("/admin/phones/blocklist", name="phone_blocklist_list")
+ * @Template()
+ */
+ public function listAction(Request $request)
+ {
+ $form = $this->createForm(SearchPhoneNumberType::class);
+
+ $form->handleRequest($request);
+ if ($form->isSubmitted() && $form->isValid()) {
+ $data = $form->getData();
+
+ $grid = new GridHelper();
+ $grid->setId('person-grid');
+ $grid->setPerPage(5);
+ $grid->setMaxResult(5);
+ $grid->setInfiniteGrid(true);
+ $grid->setRoute('lc_admin_person_grid');
+ $grid->setRouteParams([$form->getName()]);
+
+ if ($data['phone']) {
+ /** @var BlockedPhoneNumberRepository $blockedPhoneRepo */
+ $blockedPhoneRepo = $this->getDoctrine()->getRepository(BlockedPhoneNumber::class);
+ $query = $blockedPhoneRepo->getSearchByPartialPhoneQuery($data['phone']);
+ $grid->setQueryBuilder($query);
+ }
+
+ $gridView = $grid->createView($request);
+ }
+
+ return [
+ 'form' => $form->createView(),
+ 'grid' => $gridView ?? null,
+ ];
+ }
+
+ /**
+ * @Route("/admin/phones/blocklist/new", name="phone_blocklist_new")
+ * @Template()
+ */
+ public function newAction(Request $request)
+ {
+ $blockRequest = new BlockPhoneNumberRequest($this->getUser());
+
+ $blockedPhones = [];
+ $form = $this->createForm(BlockPhoneFormType::class, $blockRequest);
+
+ $form->handleRequest($request);
+ if ($form->isSubmitted() && $form->isValid()) {
+ $phoneNumber = $blockRequest->phoneNumber;
+
+ $blocklist = $this->getBlocklistService();
+ $blockedPhoneNumber = $blocklist->addBlockedPhoneNumber($phoneNumber, $blockRequest->getBlockedBy());
+ $blockedPhones = $blocklist->checkPhoneNumber($blockedPhoneNumber->getPhoneNumber());
+
+ /** @var Session $session */
+ $session = $request->getSession();
+ $session->getFlashBag()->add('success', "Phone number successfully banned.");
+ if (($count = count($blockedPhones)) > 0) {
+ $session->getFlashBag()->add('success', "{$count} accounts were blocked blocked.");
+ }
+
+ return $this->redirectToRoute('phone_blocklist_list');
+ }
+
+ return [
+ 'form' => $form->createView(),
+ 'blockedPhones' => $blockedPhones,
+ ];
+ }
+
+ /**
+ * @Route("/admin/phones/blocklist/{phone}", name="phone_blocklist_details", requirements={"phone": "[0-9+]+"})
+ * @Template()
+ */
+ public function detailsAction(Request $request, string $phone)
+ {
+ $phoneNumber = $this->parsePhone($phone);
+ $blockedPhone = $this->getOr404($phoneNumber);
+
+ return [
+ 'blockedPhone' => $blockedPhone,
+ 'grid' => $this->getUsersByPhoneGrid($request, $phoneNumber),
+ ];
+ }
+
+ /**
+ * @return BlocklistInterface
+ */
+ private function getBlocklistService(): BlocklistInterface
+ {
+ /** @var BlocklistInterface $blocklistService */
+ $blocklistService = $this->get('phone_verification.blocklist');
+
+ return $blocklistService;
+ }
+
+ private function parsePhone($phone): PhoneNumber
+ {
+ $phoneUtils = PhoneNumberUtil::getInstance();
+ try {
+ return $phoneUtils->parse($phone);
+ } catch (NumberParseException $e) {
+ throw new BadRequestHttpException("Invalid phone number");
+ }
+ }
+
+ private function getOr404(PhoneNumber $phoneNumber): BlockedPhoneNumberInterface
+ {
+ $blocklistService = $this->getBlocklistService();
+ $blockedPhone = $blocklistService->getBlockedPhoneNumberByPhone($phoneNumber);
+
+ if (null === $blockedPhone) {
+ throw new NotFoundHttpException("Blocked Phone Number not found");
+ }
+
+ return $blockedPhone;
+ }
+
+ private function getUsersByPhoneGrid(Request $request, PhoneNumber $phoneNumber)
+ {
+ $grid = new GridHelper();
+ $grid->setId('person-grid');
+ $grid->setPerPage(5);
+ $grid->setMaxResult(5);
+ $grid->setInfiniteGrid(true);
+ $grid->setRoute('phone_blocklist_details');
+ $grid->setRouteParams(['phone']);
+
+ /** @var PersonRepository $repo */
+ $repo = $this->getDoctrine()->getRepository('LoginCidadaoCoreBundle:Person');
+ $query = $repo->getPhoneSearchQuery($phoneNumber);
+ $grid->setQueryBuilder($query);
+
+ return $grid->createView($request);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Controller/VerificationController.php b/src/LoginCidadao/PhoneVerificationBundle/Controller/VerificationController.php
index 61d06ce02..61ca70125 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Controller/VerificationController.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Controller/VerificationController.php
@@ -10,13 +10,23 @@
namespace LoginCidadao\PhoneVerificationBundle\Controller;
+use FOS\UserBundle\Event\FilterUserResponseEvent;
+use FOS\UserBundle\Event\FormEvent;
+use FOS\UserBundle\Event\GetResponseUserEvent;
+use FOS\UserBundle\FOSUserEvents;
+use FOS\UserBundle\Model\UserManagerInterface;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\CoreBundle\Security\User\Manager\UserManager;
use LoginCidadao\PhoneVerificationBundle\Exception\VerificationNotSentException;
+use LoginCidadao\PhoneVerificationBundle\Form\PhoneNumberFormType;
+use LoginCidadao\PhoneVerificationBundle\Form\PhoneVerificationType;
use LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface;
use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationServiceInterface;
use LoginCidadao\TaskStackBundle\Service\TaskStackManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
@@ -43,7 +53,10 @@ public function verifyAction(Request $request, $id)
return $this->noVerificationOrVerified($request);
}
- $form = $this->createForm('LoginCidadao\PhoneVerificationBundle\Form\PhoneVerificationType');
+ $editPhoneForm = $this->createForm(PhoneNumberFormType::class, $verification->getPerson(), [
+ 'action' => $this->generateUrl('lc_phone_verification_edit_phone', ['verificationId' => $id]),
+ ]);
+ $form = $this->createForm(PhoneVerificationType::class);
$form->handleRequest($request);
$verified = false;
@@ -62,8 +75,15 @@ public function verifyAction(Request $request, $id)
}
$nextResend = $this->getNextResendDate($verification);
+ $mandatory = $phoneVerificationService->isVerificationMandatory($verification);
- return ['verification' => $verification, 'nextResend' => $nextResend, 'form' => $form->createView()];
+ return [
+ 'verification' => $verification,
+ 'nextResend' => $nextResend,
+ 'mandatory' => $mandatory,
+ 'form' => $form->createView(),
+ 'editPhoneForm' => $editPhoneForm->createView(),
+ ];
}
/**
@@ -112,6 +132,41 @@ public function resendAction(Request $request, $id)
return $this->redirectToRoute('lc_verify_phone', ['id' => $id]);
}
+ /**
+ * @Route("/task/verify-phone/{verificationId}/edit-phone", name="lc_phone_verification_edit_phone")
+ */
+ public function editPhoneAction(Request $request, $verificationId)
+ {
+ /** @var PersonInterface $person */
+ $person = $this->getUser();
+ $response = $this->redirectToRoute('lc_verify_phone', ['id' => $verificationId]);
+
+ /** @var EventDispatcherInterface $dispatcher */
+ $dispatcher = $this->get('event_dispatcher');
+ $event = new GetResponseUserEvent($person, $request);
+ $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_INITIALIZE, $event);
+
+ $editPhoneForm = $this->createForm(PhoneNumberFormType::class, $person, [
+ 'action' => $this->generateUrl('lc_phone_verification_edit_phone', ['verificationId' => $verificationId]),
+ ]);
+ $editPhoneForm->handleRequest($request);
+ if ($editPhoneForm->isValid()) {
+ /** @var $userManager UserManager */
+ $userManager = $this->get('lc.user_manager');
+
+ $event = new FormEvent($editPhoneForm, $request);
+ $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_SUCCESS, $event);
+
+ $userManager->updateUser($person);
+
+ $event = new FilterUserResponseEvent($person, $request, $response);
+ $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_COMPLETED, $event);
+ $response = $event->getResponse();
+ }
+
+ return $response;
+ }
+
/**
* @Route("/task/verify-phone/{id}/skip", name="lc_phone_verification_skip")
* @Template()
diff --git a/src/LoginCidadao/PhoneVerificationBundle/DependencyInjection/Configuration.php b/src/LoginCidadao/PhoneVerificationBundle/DependencyInjection/Configuration.php
index c37e5d747..5df9a4b29 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/DependencyInjection/Configuration.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/DependencyInjection/Configuration.php
@@ -25,6 +25,10 @@ public function getConfigTreeBuilder()
->booleanNode('enabled')
->defaultFalse()
->end()
+ // User's will HAVE TO verify their phone number after X accounts are using it
+ ->scalarNode('require_validation_threshold')
+ ->defaultValue(3)
+ ->end()
->arrayNode('verification_code')
->addDefaultsIfNotSet()
->children()
@@ -63,6 +67,19 @@ public function getConfigTreeBuilder()
->end()
->end()
->end()
+
+ ->arrayNode('blocklist')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->booleanNode('enable_auto_block')
+ ->defaultValue(true)
+ ->end()
+ // A phone will get block-listed after X accounts are using it
+ ->scalarNode('auto_block_limit')
+ ->defaultValue(10)
+ ->end()
+ ->end()
+ ->end()
->end();
// Here you should define the parameters that are allowed to
diff --git a/src/LoginCidadao/PhoneVerificationBundle/DependencyInjection/LoginCidadaoPhoneVerificationExtension.php b/src/LoginCidadao/PhoneVerificationBundle/DependencyInjection/LoginCidadaoPhoneVerificationExtension.php
index 575c6d41e..4afe1bdfc 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/DependencyInjection/LoginCidadaoPhoneVerificationExtension.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/DependencyInjection/LoginCidadaoPhoneVerificationExtension.php
@@ -23,6 +23,7 @@ public function load(array $configs, ContainerBuilder $container)
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter("lc.phone_verification.options.enabled", $config['enabled']);
+ $container->setParameter("lc.phone_verification.options.require_validation_threshold", $config['require_validation_threshold']);
foreach ($config['verification_code'] as $key => $value) {
$container->setParameter("lc.phone_verification.options.code.{$key}", $value);
@@ -33,6 +34,9 @@ public function load(array $configs, ContainerBuilder $container)
foreach ($config['sms'] as $key => $value) {
$container->setParameter("lc.phone_verification.options.sms.{$key}", $value);
}
+ foreach ($config['blocklist'] as $key => $value) {
+ $container->setParameter("lc.phone_verification.options.blocklist.{$key}", $value);
+ }
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Entity/BlockedPhoneNumber.php b/src/LoginCidadao/PhoneVerificationBundle/Entity/BlockedPhoneNumber.php
new file mode 100644
index 000000000..444ba8822
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Entity/BlockedPhoneNumber.php
@@ -0,0 +1,88 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Entity;
+
+use Doctrine\ORM\Mapping as ORM;
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockedPhoneNumberInterface;
+
+/**
+ * Class BlockedPhoneNumber
+ *
+ * @package LoginCidadao\PhoneVerificationBundle\Model
+ *
+ * @ORM\Entity(repositoryClass="LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumberRepository")
+ * @ORM\Table(name="blocked_phone_number", indexes={
+ * @ORM\Index(name="blocked_phone_number_idx", columns={"phone_number"})
+ * })
+ * @ORM\HasLifecycleCallbacks
+ */
+class BlockedPhoneNumber implements BlockedPhoneNumberInterface
+{
+ /**
+ * @var integer
+ *
+ * @ORM\Column(name="id", type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
+ */
+ private $id;
+
+ /**
+ * @var PhoneNumber
+ *
+ * @ORM\Column(name="phone_number", type="phone_number", nullable=false, unique=true)
+ */
+ private $phoneNumber;
+
+ /**
+ * @var PersonInterface
+ *
+ * @ORM\ManyToOne(targetEntity="LoginCidadao\CoreBundle\Entity\Person")
+ * @ORM\JoinColumn(name="blocked_by_person_id", referencedColumnName="id", unique=false)
+ */
+ private $blockedBy;
+
+ /**
+ * @var \DateTime
+ *
+ * @ORM\Column(name="created_at", type="datetime", nullable=true)
+ */
+ private $createdAt;
+
+ public function __construct(PhoneNumber $phoneNumber, PersonInterface $blockedBy, \DateTime $createdAt)
+ {
+ $this->phoneNumber = $phoneNumber;
+ $this->blockedBy = $blockedBy;
+ $this->createdAt = $createdAt;
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getPhoneNumber(): PhoneNumber
+ {
+ return $this->phoneNumber;
+ }
+
+ public function getBlockedBy(): PersonInterface
+ {
+ return $this->blockedBy;
+ }
+
+ public function getCreatedAt(): \DateTime
+ {
+ return $this->createdAt;
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Entity/BlockedPhoneNumberRepository.php b/src/LoginCidadao/PhoneVerificationBundle/Entity/BlockedPhoneNumberRepository.php
new file mode 100644
index 000000000..0b0c4a48c
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Entity/BlockedPhoneNumberRepository.php
@@ -0,0 +1,61 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Entity;
+
+use Doctrine\ORM\EntityRepository;
+use libphonenumber\PhoneNumber;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockedPhoneNumberInterface;
+
+/**
+ * Class BlockedPhoneNumberRepository
+ * @package LoginCidadao\PhoneVerificationBundle\Entity
+ *
+ * @codeCoverageIgnore
+ */
+class BlockedPhoneNumberRepository extends EntityRepository
+{
+ /**
+ * @param PhoneNumber $phoneNumber
+ * @return BlockedPhoneNumberInterface
+ */
+ public function findByPhone(PhoneNumber $phoneNumber): ?BlockedPhoneNumberInterface
+ {
+ /** @var BlockedPhoneNumberInterface $blockedPhone */
+ $blockedPhone = $this->findOneBy(['phoneNumber' => $phoneNumber]);
+
+ return $blockedPhone;
+ }
+
+ /**
+ * @param string $search
+ * @return BlockedPhoneNumberInterface[]
+ */
+ public function searchBlocksByPartialPhone(string $search): array
+ {
+
+ return $this->getSearchByPartialPhoneQuery($search)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function getSearchByPartialPhoneQuery(string $search)
+ {
+ if ($search[0] === '+') {
+ $search = "{$search}%";
+ } else {
+ $search = "%{$search}%";
+ }
+
+ return $this->createQueryBuilder('b')
+ ->where('b.phoneNumber LIKE :search')
+ ->setParameter('search', $search);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Entity/PhoneVerificationRepository.php b/src/LoginCidadao/PhoneVerificationBundle/Entity/PhoneVerificationRepository.php
index 0b77fab24..d0c2fd16a 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Entity/PhoneVerificationRepository.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Entity/PhoneVerificationRepository.php
@@ -11,6 +11,10 @@
namespace LoginCidadao\PhoneVerificationBundle\Entity;
use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\NonUniqueResultException;
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\Person;
+use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
/**
* PhoneVerificationRepository
@@ -24,10 +28,31 @@ class PhoneVerificationRepository extends EntityRepository
{
public function countBadges()
{
- return $this->createQueryBuilder('ph')
- ->select('COUNT(p)')
- ->innerJoin('LoginCidadaoCoreBundle:Person', 'p', 'WITH', 'ph.person = p')
- ->andWhere('ph.verifiedAt IS NOT NULL')
- ->getQuery()->getSingleScalarResult();
+ try {
+ return $this->createQueryBuilder('ph')
+ ->select('COUNT(p)')
+ ->innerJoin(Person::class, 'p', 'WITH', 'ph.person = p')
+ ->andWhere('ph.verifiedAt IS NOT NULL')
+ ->getQuery()->getSingleScalarResult();
+ } catch (NonUniqueResultException $e) {
+ throw new \RuntimeException('Could not count how many people has a verified phone number', 0, $e);
+ }
+ }
+
+ public function countVerified(PhoneNumber $phoneNumber)
+ {
+ try {
+ return $this->createQueryBuilder('v')
+ ->select('COUNT(p)')
+ ->innerJoin(Person::class, 'p', 'WITH', 'v.person = p')
+ ->where('v.phone = :phone')
+ ->andWhere('v.phone = p.mobile')
+ ->andWhere('v.verifiedAt IS NOT NULL')
+ ->setParameter('phone', $phoneNumber, PhoneNumberType::NAME)
+ ->getQuery()
+ ->getSingleScalarResult();
+ } catch (NonUniqueResultException $e) {
+ throw new \RuntimeException('Could not count how many people uses the same verified phone number', 0, $e);
+ }
}
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Event/BlocklistSubscriber.php b/src/LoginCidadao/PhoneVerificationBundle/Event/BlocklistSubscriber.php
new file mode 100644
index 000000000..393de5d88
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Event/BlocklistSubscriber.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Event;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\PhoneVerificationBundle\PhoneVerificationEvents;
+use LoginCidadao\PhoneVerificationBundle\Service\BlocklistInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
+use Symfony\Component\Security\Http\SecurityEvents;
+
+class BlocklistSubscriber implements EventSubscriberInterface
+{
+ /** @var BlocklistInterface */
+ private $blocklist;
+
+ /** @var AuthorizationCheckerInterface */
+ private $authChecker;
+
+ /**
+ * BlocklistSubscriber constructor.
+ * @param BlocklistInterface $blocklist
+ * @param AuthorizationCheckerInterface $authorizationChecker
+ */
+ public function __construct(BlocklistInterface $blocklist, AuthorizationCheckerInterface $authorizationChecker)
+ {
+ $this->blocklist = $blocklist;
+ $this->authChecker = $authorizationChecker;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSubscribedEvents()
+ {
+ return [
+ PhoneVerificationEvents::PHONE_CHANGED => 'onPhoneChange',
+ SecurityEvents::INTERACTIVE_LOGIN => 'onLogin',
+ ];
+ }
+
+ /**
+ * @param PhoneChangedEvent $event
+ */
+ public function onPhoneChange(PhoneChangedEvent $event)
+ {
+ $phone = $event->getPerson()->getMobile();
+ $this->checkPhone($phone);
+ }
+
+ /**
+ * @param InteractiveLoginEvent $event
+ */
+ public function onLogin(InteractiveLoginEvent $event)
+ {
+ $person = $event->getAuthenticationToken()->getUser();
+ if ($person instanceof PersonInterface) {
+ $phone = $person->getMobile();
+ $this->checkPhone($phone);
+ }
+ }
+
+ private function checkPhone(?PhoneNumber $phoneNumber)
+ {
+ if ($phoneNumber instanceof PhoneNumber && $this->authChecker->isGranted('FEATURE_PHONE_BLOCKLIST')) {
+ $this->blocklist->checkPhoneNumber($phoneNumber);
+ }
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Event/PhoneVerificationSubscriber.php b/src/LoginCidadao/PhoneVerificationBundle/Event/PhoneVerificationSubscriber.php
index 13a0c4f5d..89720bf5f 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Event/PhoneVerificationSubscriber.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Event/PhoneVerificationSubscriber.php
@@ -10,8 +10,6 @@
namespace LoginCidadao\PhoneVerificationBundle\Event;
-
-use FOS\UserBundle\FOSUserEvents;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use LoginCidadao\CoreBundle\Model\PersonInterface;
@@ -92,14 +90,11 @@ public function onPhoneChange(PhoneChangedEvent $event, $eventName, EventDispatc
$newPhone = $person->getMobile();
$phoneUtil = PhoneNumberUtil::getInstance();
- $this->info(
- 'Phone changed from {old} to {new} for user {id}',
- [
- 'id' => $person->getId(),
- 'old' => $oldPhone ? $phoneUtil->format($oldPhone, PhoneNumberFormat::E164) : null,
- 'new' => $newPhone ? $phoneUtil->format($newPhone, PhoneNumberFormat::E164) : null,
- ]
- );
+ $this->info('Phone changed from {old} to {new} for user {id}', [
+ 'id' => $person->getId(),
+ 'old' => $oldPhone ? $phoneUtil->format($oldPhone, PhoneNumberFormat::E164) : null,
+ 'new' => $newPhone ? $phoneUtil->format($newPhone, PhoneNumberFormat::E164) : null,
+ ]);
if ($oldPhone) {
$oldPhoneVerification = $this->phoneVerificationService->getPhoneVerification($person, $oldPhone);
@@ -112,10 +107,7 @@ public function onPhoneChange(PhoneChangedEvent $event, $eventName, EventDispatc
}
if ($newPhone) {
- $phoneVerification = $this->phoneVerificationService->createPhoneVerification(
- $person,
- $newPhone
- );
+ $phoneVerification = $this->phoneVerificationService->createPhoneVerification($person, $newPhone);
$sendEvent = new SendPhoneVerificationEvent($phoneVerification);
$dispatcher->dispatch(PhoneVerificationEvents::PHONE_VERIFICATION_REQUESTED, $sendEvent);
@@ -126,13 +118,10 @@ public function onVerificationRequest(SendPhoneVerificationEvent $event)
{
$person = $event->getPhoneVerification()->getPerson();
$phoneUtil = PhoneNumberUtil::getInstance();
- $this->info(
- 'Phone Verification requested for {phone} for user {user_id}',
- [
- 'user_id' => $person->getId(),
- 'phone' => $phoneUtil->format($person->getMobile(), PhoneNumberFormat::E164),
- ]
- );
+ $this->info('Phone Verification requested for {phone} for user {user_id}', [
+ 'user_id' => $person->getId(),
+ 'phone' => $phoneUtil->format($person->getMobile(), PhoneNumberFormat::E164),
+ ]);
}
public function onCodeSent(SendPhoneVerificationEvent $event)
@@ -153,9 +142,7 @@ public function onLogin(InteractiveLoginEvent $event, $eventName, EventDispatche
if (!$verification) {
$this->info(
"User {user_id} has a phone but didn't verify it. Creating Phone Verification request.",
- [
- 'user_id' => $person->getId(),
- ]
+ ['user_id' => $person->getId()]
);
$verification = $this->phoneVerificationService->enforcePhoneVerification($person, $phone);
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Event/TaskSubscriber.php b/src/LoginCidadao/PhoneVerificationBundle/Event/TaskSubscriber.php
index 1e0e3370d..f5cbcc4f2 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Event/TaskSubscriber.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Event/TaskSubscriber.php
@@ -94,6 +94,8 @@ public function onGetTasks(GetTasksEvent $event)
}
}
- $event->addTaskIfStackEmpty(new ConfirmPhoneTask($pendingVerification->getId()));
+ $isMandatory = $this->phoneVerificationService->isVerificationMandatory($pendingVerification);
+ $task = new ConfirmPhoneTask($pendingVerification->getId(), $isMandatory);
+ $event->addTaskIfStackEmpty($task);
}
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Form/BlockPhoneFormType.php b/src/LoginCidadao/PhoneVerificationBundle/Form/BlockPhoneFormType.php
new file mode 100644
index 000000000..6f80ed5ab
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Form/BlockPhoneFormType.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Form;
+
+use libphonenumber\PhoneNumberFormat;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockPhoneNumberRequest;
+use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class BlockPhoneFormType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('phoneNumber', PhoneNumberType::class, [
+ 'required' => true,
+ 'label' => 'admin.blocklist.new.form.phone.label',
+ 'attr' => [
+ 'class' => 'form-control intl-tel',
+ 'placeholder' => 'admin.blocklist.new.form.phone.placeholder',
+ ],
+ 'label_attr' => ['class' => 'intl-tel-label'],
+ 'format' => PhoneNumberFormat::E164,
+ ])
+ ->add('submit', SubmitType::class, [
+ 'label' => 'admin.blocklist.new.form.submit.label',
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'data_class' => BlockPhoneNumberRequest::class,
+ ]);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Form/PhoneNumberFormType.php b/src/LoginCidadao/PhoneVerificationBundle/Form/PhoneNumberFormType.php
new file mode 100644
index 000000000..a981de00a
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Form/PhoneNumberFormType.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Form;
+
+use libphonenumber\PhoneNumberFormat;
+use LoginCidadao\CoreBundle\Entity\Person;
+use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class PhoneNumberFormType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('mobile', PhoneNumberType::class, [
+ 'required' => false,
+ 'label' => 'person.form.mobile.label',
+ 'attr' => [
+ 'class' => 'form-control intl-tel',
+ 'placeholder' => 'person.form.mobile.placeholder',
+ ],
+ 'label_attr' => ['class' => 'intl-tel-label'],
+ 'format' => PhoneNumberFormat::E164,
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'data_class' => Person::class,
+ 'validation_groups' => ['LoginCidadaoEmailForm'],
+ ]);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Form/PhoneVerificationType.php b/src/LoginCidadao/PhoneVerificationBundle/Form/PhoneVerificationType.php
index 0c0dd9106..ef06da665 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Form/PhoneVerificationType.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Form/PhoneVerificationType.php
@@ -10,8 +10,10 @@
namespace LoginCidadao\PhoneVerificationBundle\Form;
+use LoginCidadao\CoreBundle\Form\Type\TelType;
use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationOptions;
use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class PhoneVerificationType extends AbstractType
@@ -34,20 +36,14 @@ public function buildForm(FormBuilderInterface $builder, array $options)
|| $this->phoneVerificationOptions->isUseLowerCase();
if ($useLetters) {
- $type = 'Symfony\Component\Form\Extension\Core\Type\TextType';
+ $type = TextType::class;
} else {
- $type = 'LoginCidadao\CoreBundle\Form\Type\TelType';
+ $type = TelType::class;
}
- $builder->add(
- 'verificationCode',
- $type,
- [
- 'label' => 'tasks.verify_phone.form.verificationCode.label',
- 'attr' => [
- 'placeholder' => 'tasks.verify_phone.form.verificationCode.placeholder',
- ],
- ]
- );
+ $builder->add('verificationCode', $type, [
+ 'label' => 'tasks.verify_phone.form.verificationCode.label',
+ 'attr' => ['placeholder' => 'tasks.verify_phone.form.verificationCode.placeholder'],
+ ]);
}
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Form/SearchPhoneNumberType.php b/src/LoginCidadao/PhoneVerificationBundle/Form/SearchPhoneNumberType.php
new file mode 100644
index 000000000..4d9e5b5b2
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Form/SearchPhoneNumberType.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Form;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\Extension\Core\Type\TelType;
+use Symfony\Component\Form\FormBuilderInterface;
+
+class SearchPhoneNumberType extends AbstractType
+{
+ /**
+ * @inheritDoc
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('phone', TelType::class, [
+ 'required' => true,
+ 'label' => 'admin.blocklist.list.form.phone.label',
+ 'attr' => [
+ 'pattern' => '[0-9+]*',
+ 'autocomplete' => 'off',
+ 'placeholder' => 'admin.blocklist.list.form.phone.placeholder',
+ ],
+ ])
+ ->add('submit', SubmitType::class, [
+ 'label' => 'admin.blocklist.list.form.submit.label',
+ ]);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Model/BlockPhoneNumberRequest.php b/src/LoginCidadao/PhoneVerificationBundle/Model/BlockPhoneNumberRequest.php
new file mode 100644
index 000000000..be7aa3afd
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Model/BlockPhoneNumberRequest.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Model;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\ValidationBundle\Validator\Constraints as LCAssert;
+use Symfony\Component\Validator\Constraints as Assert;
+use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as AssertPhoneNumber;
+
+class BlockPhoneNumberRequest
+{
+ /**
+ * @var PhoneNumber
+ * @Assert\NotBlank()
+ * @LCAssert\E164PhoneNumber(maxMessage="person.validation.mobile.length.max")
+ * @AssertPhoneNumber()
+ */
+ public $phoneNumber;
+
+ /**
+ * @var PersonInterface
+ * @Assert\NotBlank()
+ */
+ private $blockedBy;
+
+ /**
+ * BlockPhoneNumberRequest constructor.
+ * @param PersonInterface $blockedBy
+ */
+ public function __construct(PersonInterface $blockedBy)
+ {
+ $this->blockedBy = $blockedBy;
+ }
+
+ /**
+ * @return PersonInterface
+ */
+ public function getBlockedBy(): PersonInterface
+ {
+ return $this->blockedBy;
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Model/BlockedPhoneNumberInterface.php b/src/LoginCidadao/PhoneVerificationBundle/Model/BlockedPhoneNumberInterface.php
new file mode 100644
index 000000000..9909f8b6a
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Model/BlockedPhoneNumberInterface.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Model;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+
+interface BlockedPhoneNumberInterface
+{
+ /**
+ * BlockedPhoneNumberInterface constructor.
+ * @param PhoneNumber $phoneNumber
+ * @param PersonInterface $blockedBy
+ * @param \DateTime $createdAt
+ */
+ public function __construct(PhoneNumber $phoneNumber, PersonInterface $blockedBy, \DateTime $createdAt);
+
+ public function getPhoneNumber(): PhoneNumber;
+
+ /**
+ * @return PersonInterface
+ */
+ public function getBlockedBy(): PersonInterface;
+
+ /**
+ * @return \DateTime
+ */
+ public function getCreatedAt(): \DateTime;
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Model/ConfirmPhoneTask.php b/src/LoginCidadao/PhoneVerificationBundle/Model/ConfirmPhoneTask.php
index 5fe587bf3..3cf4b5925 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Model/ConfirmPhoneTask.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Model/ConfirmPhoneTask.php
@@ -22,13 +22,18 @@ class ConfirmPhoneTask extends AbstractTask
/** @var TaskTargetInterface */
private $target;
+ /** @var bool */
+ private $mandatory;
+
/**
* ConfirmPhoneTask constructor.
* @param mixed $verificationId
+ * @param bool $mandatory
*/
- public function __construct($verificationId)
+ public function __construct($verificationId, bool $mandatory = false)
{
$this->verificationId = $verificationId;
+ $this->mandatory = $mandatory;
$this->target = new RouteTaskTarget('lc_verify_phone', ['id' => $verificationId]);
}
@@ -54,6 +59,7 @@ public function getRoutes()
'lc_phone_verification_code_resend',
'lc_phone_verification_verify_link',
'lc_phone_verification_skip',
+ 'lc_phone_verification_edit_phone',
];
}
@@ -70,6 +76,6 @@ public function getTarget()
*/
public function isMandatory()
{
- return false;
+ return $this->mandatory;
}
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/config/services.xml b/src/LoginCidadao/PhoneVerificationBundle/Resources/config/services.xml
deleted file mode 100644
index 249499582..000000000
--- a/src/LoginCidadao/PhoneVerificationBundle/Resources/config/services.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/config/services.yml b/src/LoginCidadao/PhoneVerificationBundle/Resources/config/services.yml
index 24bb06720..d470cea9b 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Resources/config/services.yml
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/config/services.yml
@@ -9,6 +9,13 @@ services:
- "%lc.phone_verification.options.code.use_upper%"
- "%lc.phone_verification.options.sms.resend_timeout%"
- "%lc.phone_verification.options.token.length%"
+ - "%lc.phone_verification.options.require_validation_threshold%"
+ lazy: true
+
+ phone_verification.blocklist.options:
+ class: LoginCidadao\PhoneVerificationBundle\Service\BlocklistOptions
+ arguments:
+ - "%lc.phone_verification.options.blocklist.auto_block_limit%"
lazy: true
phone_verification:
@@ -68,6 +75,14 @@ services:
tags:
- { name: kernel.event_subscriber }
+ phone_verification.blocklist.subscriber:
+ class: LoginCidadao\PhoneVerificationBundle\Event\BlocklistSubscriber
+ arguments:
+ - "@phone_verification.blocklist"
+ - "@security.authorization_checker"
+ tags:
+ - { name: kernel.event_subscriber }
+
phone_verification.form:
class: LoginCidadao\PhoneVerificationBundle\Form\PhoneVerificationType
arguments:
@@ -87,3 +102,12 @@ services:
- "@doctrine.orm.entity_manager"
- "@event_dispatcher"
- "@phone_verification.sent_verification.repository"
+
+ phone_verification.blocklist:
+ class: LoginCidadao\PhoneVerificationBundle\Service\Blocklist
+ arguments:
+ - "@lc.user_manager"
+ - "@lc.mailer"
+ - "@doctrine.orm.entity_manager"
+ - "@phone_verification"
+ - "@phone_verification.blocklist.options"
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/translations/messages.en.yml b/src/LoginCidadao/PhoneVerificationBundle/Resources/translations/messages.en.yml
index 401807177..a139edcb6 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Resources/translations/messages.en.yml
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/translations/messages.en.yml
@@ -31,3 +31,46 @@ tasks:
errors:
too_many_requests: It's not possible to resend the verification code yet. Please, retry later.
unavailable: The SMS service is not available right now. Please, wait a few minutes and try again.
+
+ edit:
+ show_form: Click here to change your phone number
+ submit: Change Phone Number
+
+admin:
+ blocklist:
+ menu_item: Ban Phone Numbers
+ list:
+ title: Search Manually Banned Phone Numbers
+ new: Ban a Phone Number
+ form:
+ phone:
+ label: Phone to be searched
+ placeholder: Type the phone you want to search
+ submit.label: Search
+ table:
+ header:
+ phone_number: Phone number
+ blocked_by: Banned by
+ date_blocked: Date banned
+ load_more: Load more
+ details: Details
+ date_blocked_format: m/d/Y H:i:s
+ new:
+ title: Ban users by phone
+ form:
+ phone:
+ label: Phone number that will be banned
+ placeholder: Type the phone number that will be banned
+ submit.label: Ban Phone Number
+ details:
+ title: "Banned Phone Details: %phone%"
+ blocked_by: Banned by:
+ date_blocked: Date banned:
+ date_blocked_format: m/d/Y H:i:s
+ users_table:
+ header:
+ full_name: Full Name
+ cpf: "Tax Payer's ID (CPF)"
+ email: Email
+ user_details: Details
+ load_more: Load more
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/translations/messages.pt_BR.yml b/src/LoginCidadao/PhoneVerificationBundle/Resources/translations/messages.pt_BR.yml
index 91c2b59c2..90c8eccbf 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Resources/translations/messages.pt_BR.yml
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/translations/messages.pt_BR.yml
@@ -31,3 +31,46 @@ tasks:
errors:
too_many_requests: Ainda não é possível reenviar o código de verificação. Aguarde alguns minutos e tente novamente.
unavailable: O serviço de SMS está indisponível no momento. Por favor, aguarde alguns minutos e tente novamente.
+
+ edit:
+ show_form: Clique aqui para alterar seu telefone
+ submit: Alterar Telefone
+
+admin:
+ blocklist:
+ menu_item: Bloqueio de Telefones
+ list:
+ title: Busca de Telefones Banidos Manualmente
+ new: Banir um Telefone
+ form:
+ phone:
+ label: Telefone a ser pesquisado
+ placeholder: Digite o telefone que deseja pesquisar
+ submit.label: Pesquisar
+ table:
+ header:
+ phone_number: Telefone
+ blocked_by: Banido por
+ date_blocked: Banido em
+ load_more: Carregar mais
+ details: Detalhes
+ date_blocked_format: d/m/Y H:i:s
+ new:
+ title: Banir usuários por telefone
+ form:
+ phone:
+ label: Telefone que será banido
+ placeholder: Digite o telefone a ser banido
+ submit.label: Banir Telefone
+ details:
+ title: "Detalhes de Telefone Banido: %phone%"
+ blocked_by: Banido por:
+ date_blocked: Banido em:
+ date_blocked_format: d/m/Y H:i:s
+ users_table:
+ header:
+ full_name: Nome Completo
+ cpf: CPF
+ email: Email
+ user_details: Detalhes
+ load_more: Carregar mais
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/blockedUsers.grid.html.twig b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/blockedUsers.grid.html.twig
new file mode 100644
index 000000000..27d65e3e1
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/blockedUsers.grid.html.twig
@@ -0,0 +1,68 @@
+{% extends "LoginCidadaoCoreBundle::grid_layout.html.twig" %}
+
+{% block grid_header_data %}
+ {{ 'admin.blocklist.details.users_table.header.full_name' | trans }}
+ {{ 'admin.blocklist.details.users_table.header.cpf' | trans }}
+ {{ 'admin.blocklist.details.users_table.header.email' | trans }}
+{% endblock grid_header_data %}
+
+{% block grid_row_action %}
+ {% set formattedPhone = row.phoneNumber | phone_number_format('E164') %}
+
+{% endblock grid_row_action %}
+
+{% block grid_row_data %}
+ {% if row.phoneNumber.countryCode == 55 %}
+ {% set format = 'NATIONAL' %}
+ {% else %}
+ {% set format = 'INTERNATIONAL' %}
+ {% endif %}
+
+
+
{{ 'admin.blocklist.details.users_table.header.full_name' | trans }}
+ {{ row.fullName }}
+
+
+
{{ 'admin.blocklist.details.users_table.header.cpf' | trans }}
+
+ {% if row.cpf is not null %}
+ {% if is_granted('ROLE_VIEW_USERS_CPF') %}
+ {{ row.cpf }}
+ {% else %}
+ {{ 'lc.admin.person.metadata.hasCpf.yes' | trans }}
+ {% endif %}
+ {% else %}
+ {{ 'lc.admin.person.metadata.hasCpf.no' | trans }}
+ {% endif %}
+
+
+
+
{{ 'admin.blocklist.details.users_table.header.email' | trans }}
+ {{ row.email }}
+
+{% endblock grid_row_data %}
+
+{% block grid_infinite_pagination %}
+
+ {% if not grid.getRlast and grid.isInfiniteGrid %}
+ {% set routeParams = { 'page': grid.page + 1 } %}
+
+
+
+ {% endif %}
+
+{% endblock grid_infinite_pagination %}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/details.html.twig b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/details.html.twig
new file mode 100644
index 000000000..5eb13123b
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/details.html.twig
@@ -0,0 +1,34 @@
+{% extends "LoginCidadaoCoreBundle:Admin:base.html.twig" %}
+
+{% block content %}
+ {% if blockedPhone.phoneNumber.countryCode == 55 %}
+ {% set format = 'NATIONAL' %}
+ {% else %}
+ {% set format = 'INTERNATIONAL' %}
+ {% endif %}
+ {% set formattedPhone = blockedPhone.phoneNumber | phone_number_format(format) %}
+
+
+
+
{{ 'admin.blocklist.details.title' | trans({"%phone%": formattedPhone }) }}
+
+
+
+
+
+ - {{ 'admin.blocklist.details.blocked_by' | trans }}
+ - {{ blockedPhone.blockedBy.fullName }}
+
+ - {{ 'admin.blocklist.details.date_blocked' | trans }}
+ - {{ blockedPhone.createdAt | date('admin.blocklist.details.date_blocked_format' | trans) }}
+
+
+
+
+
+
+ {% if grid %}
+ {{ include("LoginCidadaoPhoneVerificationBundle:AdminBlocklist:blockedUsers.grid.html.twig") }}
+ {% endif %}
+
+{% endblock %}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/grid.html.twig b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/grid.html.twig
new file mode 100644
index 000000000..b8d153fa2
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/grid.html.twig
@@ -0,0 +1,58 @@
+{% extends "LoginCidadaoCoreBundle::grid_layout.html.twig" %}
+
+{% block grid_header_data %}
+ {{ 'admin.blocklist.list.table.header.phone_number' | trans }}
+ {{ 'admin.blocklist.list.table.header.blocked_by' | trans }}
+ {{ 'admin.blocklist.list.table.header.date_blocked' | trans }}
+{% endblock grid_header_data %}
+
+{% block grid_row_action %}
+ {% set formattedPhone = row.phoneNumber | phone_number_format('E164') %}
+
+{% endblock grid_row_action %}
+
+{% block grid_row_data %}
+ {% if row.phoneNumber.countryCode == 55 %}
+ {% set format = 'NATIONAL' %}
+ {% else %}
+ {% set format = 'INTERNATIONAL' %}
+ {% endif %}
+
+
+
{{ 'admin.blocklist.list.table.header.phone_number' | trans }}
+ {{ row.phoneNumber | phone_number_format(format) }}
+
+
+
{{ 'admin.blocklist.list.table.header.blocked_by' | trans }}
+ {{ row.blockedBy.fullName }}
+
+
+
{{ 'admin.blocklist.list.table.header.date_blocked' | trans }}
+ {{ row.createdAt | date('admin.blocklist.list.table.date_blocked_format' | trans) }}
+
+{% endblock grid_row_data %}
+
+{% block grid_infinite_pagination %}
+
+ {% if not grid.getRlast and grid.isInfiniteGrid %}
+ {% set routeParams = { 'page': grid.page + 1 } %}
+
+
+
+ {% endif %}
+
+{% endblock grid_infinite_pagination %}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/list.html.twig b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/list.html.twig
new file mode 100644
index 000000000..1c9f31149
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/list.html.twig
@@ -0,0 +1,30 @@
+{% extends "LoginCidadaoCoreBundle:Admin:base.html.twig" %}
+{% block content %}
+
+
+
+
+
+
{{ 'admin.blocklist.list.title'|trans }}
+
+
+
+
+
+
+
+ {% if grid %}
+ {{ include("LoginCidadaoPhoneVerificationBundle:AdminBlocklist:grid.html.twig") }}
+ {% endif %}
+
+{% endblock %}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/new.html.twig b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/new.html.twig
new file mode 100644
index 000000000..b7daa4b75
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/AdminBlocklist/new.html.twig
@@ -0,0 +1,41 @@
+{% extends "LoginCidadaoCoreBundle:Admin:base.html.twig" %}
+{% block content %}
+
+
+
+
{{ 'admin.blocklist.new.title'|trans }}
+
+
+
+
+{% endblock %}
+
+{% block javascripts %}
+ {{ parent() }}
+
+ {% javascripts
+ '@intl_tel_input_js'
+ '@LoginCidadaoCoreBundle/Resources/public/js/components/intl-tel-input.js'
+ filter='uglifyjs2' %}
+
+ {% endjavascripts %}
+
+
+{% endblock %}
+
+{% block stylesheets_custom %}
+ {% stylesheets '@lc_logged_css' '@intl_tel_input_css' filter='cssrewrite' filter='?uglifycss' %}
+
+ {% endstylesheets %}
+{% endblock %}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Resources/views/Verification/verify.html.twig b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/Verification/verify.html.twig
index 25992d441..bcebdb565 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Resources/views/Verification/verify.html.twig
+++ b/src/LoginCidadao/PhoneVerificationBundle/Resources/views/Verification/verify.html.twig
@@ -28,7 +28,8 @@
{# Resend #}
{% if nextResend %}
-
{{ 'tasks.verify_phone.resend.waiting.please_wait' | trans({'%date%': nextResend | date('tasks.verify_phone.resend.waiting.date_format' | trans)}) | raw }}
+
{{ 'tasks.verify_phone.resend.waiting.please_wait' | trans({'%date%': nextResend | date('tasks.verify_phone.resend.waiting.date_format' | trans)}) | raw }}
{% endif %}
@@ -43,21 +44,52 @@
{# END OF Resend #}
- {# Skip #}
-
{{ 'tasks.verify_phone.skip.open_warning' | trans }}
-
-
{{ 'tasks.verify_phone.skip.warning' | trans }}
-
{{ 'tasks.verify_phone.skip.confirm' | trans }}
+ {# Change Phone #}
+
+
+ {{ form_start(editPhoneForm) }}
+
+ {{ form_row(editPhoneForm.mobile) }}
+
+
+ {{ form_end(editPhoneForm) }}
+ {# END OF Change Phone #}
+
+ {# Skip #}
+ {% if not mandatory %}
+
{{ 'tasks.verify_phone.skip.open_warning' | trans }}
+
+ {% endif %}
{# END OF Skip #}
{% endblock %}
{% block javascripts %}
{{ parent() }}
+ {% javascripts
+ '@intl_tel_input_js'
+ '@LoginCidadaoCoreBundle/Resources/public/js/components/intl-tel-input.js'
+ filter='uglifyjs2' %}
+
+ {% endjavascripts %}
{% endblock %}
+
+{% block stylesheets_custom %}
+ {% stylesheets '@lc_compact_layout_css' '@intl_tel_input_css' filter='cssrewrite' filter='?uglifycss' %}
+
+ {% endstylesheets %}
+{% endblock %}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Service/Blocklist.php b/src/LoginCidadao/PhoneVerificationBundle/Service/Blocklist.php
new file mode 100644
index 000000000..9da898f76
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Service/Blocklist.php
@@ -0,0 +1,148 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Service;
+
+use Doctrine\ORM\EntityManagerInterface;
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Mailer\TwigSwiftMailer;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\CoreBundle\Security\User\Manager\UserManager;
+use LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumber;
+use LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumberRepository;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockedPhoneNumberInterface;
+
+class Blocklist implements BlocklistInterface
+{
+ /** @var UserManager */
+ private $userManager;
+
+ /** @var TwigSwiftMailer */
+ private $mailer;
+
+ /** @var BlockedPhoneNumberRepository */
+ private $blockedPhoneRepository;
+
+ /** @var PhoneVerificationServiceInterface */
+ private $phoneVerificationService;
+
+ /** @var EntityManagerInterface */
+ private $em;
+
+ /** @var BlocklistOptions */
+ private $options;
+
+ /**
+ * Blocklist constructor.
+ * @param UserManager $userManager
+ * @param TwigSwiftMailer $mailer
+ * @param EntityManagerInterface $em
+ * @param PhoneVerificationServiceInterface $phoneVerificationService
+ * @param BlocklistOptions $options
+ */
+ public function __construct(
+ UserManager $userManager,
+ TwigSwiftMailer $mailer,
+ EntityManagerInterface $em,
+ PhoneVerificationServiceInterface $phoneVerificationService,
+ BlocklistOptions $options
+ ) {
+ $this->userManager = $userManager;
+ $this->mailer = $mailer;
+ $this->em = $em;
+ $this->options = $options;
+ $this->blockedPhoneRepository = $this->em->getRepository(BlockedPhoneNumber::class);
+ $this->phoneVerificationService = $phoneVerificationService;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isPhoneBlocked(PhoneNumber $phoneNumber): bool
+ {
+ return $this->isManuallyBlocked($phoneNumber) || $this->isBlockedAutomatically($phoneNumber);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function blockByPhone(PhoneNumber $phoneNumber): array
+ {
+ $this->em->flush();
+ $blockedUsers = $this->userManager->blockUsersByPhone($phoneNumber, UserManager::FLUSH_STRATEGY_ONCE);
+ $this->notifyBlockedUsers($blockedUsers);
+
+ return $blockedUsers;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function checkPhoneNumber(PhoneNumber $phoneNumber): array
+ {
+ $blocked = [];
+ if ($this->isPhoneBlocked($phoneNumber)) {
+ $blocked = $this->blockByPhone($phoneNumber);
+ }
+
+ return $blocked;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addBlockedPhoneNumber(
+ PhoneNumber $phoneNumber,
+ PersonInterface $blockedBy
+ ): BlockedPhoneNumberInterface {
+ $blockedPhoneNumber = new BlockedPhoneNumber($phoneNumber, $blockedBy, new \DateTime());
+ $this->em->persist($blockedPhoneNumber);
+ $this->em->flush();
+
+ return $blockedPhoneNumber;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBlockedPhoneNumberByPhone(PhoneNumber $phoneNumber): ?BlockedPhoneNumberInterface
+ {
+ /** @var BlockedPhoneNumberInterface $blockedPhoneNumber */
+ $blockedPhoneNumber = $this->blockedPhoneRepository->findByPhone($phoneNumber);
+
+ return $blockedPhoneNumber;
+ }
+
+ /**
+ * @param PersonInterface[] $blockedUsers
+ */
+ private function notifyBlockedUsers(array $blockedUsers)
+ {
+ foreach ($blockedUsers as $person) {
+ $this->mailer->sendAccountAutoBlockedMessage($person);
+ }
+ }
+
+ private function isManuallyBlocked(PhoneNumber $phoneNumber): bool
+ {
+ return $this->blockedPhoneRepository->findByPhone($phoneNumber) instanceof BlockedPhoneNumberInterface;
+ }
+
+ private function isBlockedAutomatically(PhoneNumber $phoneNumber): bool
+ {
+ if ($this->options->isAutoBlockEnabled()) {
+ $autoBlockLimit = $this->options->getAutoBlockPhoneLimit();
+
+ return $this->phoneVerificationService->countVerified($phoneNumber) >= $autoBlockLimit;
+ }
+
+ return false;
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Service/BlocklistInterface.php b/src/LoginCidadao/PhoneVerificationBundle/Service/BlocklistInterface.php
new file mode 100644
index 000000000..f81c6a413
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Service/BlocklistInterface.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Service;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockedPhoneNumberInterface;
+
+interface BlocklistInterface
+{
+ public function isPhoneBlocked(PhoneNumber $phoneNumber): bool;
+
+ /**
+ * @param PhoneNumber $phoneNumber
+ * @return PersonInterface[]
+ */
+ public function blockByPhone(PhoneNumber $phoneNumber): array;
+
+ /**
+ * Checks if the phone is blocked. If it is, all relevant accounts will be blocked.
+ *
+ * @param PhoneNumber $phoneNumber
+ * @return PersonInterface[]
+ */
+ public function checkPhoneNumber(PhoneNumber $phoneNumber): array;
+
+ /**
+ * @param PhoneNumber $phoneNumber
+ * @param PersonInterface $blockedBy
+ * @return BlockedPhoneNumberInterface
+ */
+ public function addBlockedPhoneNumber(
+ PhoneNumber $phoneNumber,
+ PersonInterface $blockedBy
+ ): BlockedPhoneNumberInterface;
+
+ /**
+ * @param PhoneNumber $phoneNumber
+ * @return BlockedPhoneNumberInterface|null
+ */
+ public function getBlockedPhoneNumberByPhone(PhoneNumber $phoneNumber): ?BlockedPhoneNumberInterface;
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Service/BlocklistOptions.php b/src/LoginCidadao/PhoneVerificationBundle/Service/BlocklistOptions.php
new file mode 100644
index 000000000..6e96f7af0
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Service/BlocklistOptions.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Service;
+
+class BlocklistOptions
+{
+ /** @var int */
+ private $autoBlockPhoneLimit;
+
+ /**
+ * BlocklistOptions constructor.
+ * @param int $autoBlockPhoneLimit
+ */
+ public function __construct(int $autoBlockPhoneLimit)
+ {
+ $this->autoBlockPhoneLimit = $autoBlockPhoneLimit;
+ }
+
+ public function isAutoBlockEnabled(): bool
+ {
+ $limit = $this->getAutoBlockPhoneLimit();
+
+ return null !== $limit && $limit > 0;
+ }
+
+ /**
+ * @return int
+ */
+ public function getAutoBlockPhoneLimit(): int
+ {
+ return $this->autoBlockPhoneLimit;
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationOptions.php b/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationOptions.php
index 39d9b7c4f..1f3144baf 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationOptions.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationOptions.php
@@ -33,6 +33,9 @@ class PhoneVerificationOptions
/** @var string */
private $verificationTokenLength;
+ /** @var int */
+ private $enforceVerificationThreshold;
+
/**
* PhoneVerificationOptions constructor.
* @param int $length
@@ -42,6 +45,7 @@ class PhoneVerificationOptions
* @param bool $useUpperCase
* @param $smsResendTimeout
* @param $verificationTokenLength
+ * @param int $enforceVerificationThreshold
*/
public function __construct(
$length,
@@ -50,7 +54,8 @@ public function __construct(
$useLowerCase,
$useUpperCase,
$smsResendTimeout,
- $verificationTokenLength
+ $verificationTokenLength,
+ int $enforceVerificationThreshold
) {
$this->length = $length;
$this->useNumbers = $useNumbers;
@@ -59,6 +64,7 @@ public function __construct(
$this->useUpperCase = $useUpperCase;
$this->smsResendTimeout = $smsResendTimeout;
$this->verificationTokenLength = $verificationTokenLength;
+ $this->enforceVerificationThreshold = $enforceVerificationThreshold;
}
/**
@@ -113,4 +119,9 @@ public function getVerificationTokenLength()
{
return $this->verificationTokenLength;
}
+
+ public function getEnforceVerificationThreshold(): int
+ {
+ return $this->enforceVerificationThreshold;
+ }
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationService.php b/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationService.php
index 332b06d10..594db45fa 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationService.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationService.php
@@ -10,8 +10,11 @@
namespace LoginCidadao\PhoneVerificationBundle\Service;
-use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\EntityManagerInterface;
use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\Person;
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\PhoneVerificationBundle\Entity\SentVerification;
use LoginCidadao\PhoneVerificationBundle\Entity\SentVerificationRepository;
use LoginCidadao\PhoneVerificationBundle\Event\PhoneVerificationEvent;
use LoginCidadao\PhoneVerificationBundle\Event\SendPhoneVerificationEvent;
@@ -24,14 +27,13 @@
use LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerificationRepository;
use LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
-use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class PhoneVerificationService implements PhoneVerificationServiceInterface
{
/** @var PhoneVerificationOptions */
private $options;
- /** @var EntityManager */
+ /** @var EntityManagerInterface */
private $em;
/** @var PhoneVerificationRepository */
@@ -40,27 +42,29 @@ class PhoneVerificationService implements PhoneVerificationServiceInterface
/** @var SentVerificationRepository */
private $sentVerificationRepository;
+ /** @var PersonRepository */
+ private $personRepository;
+
/** @var EventDispatcherInterface */
private $dispatcher;
/**
* PhoneVerificationService constructor.
* @param PhoneVerificationOptions $options
- * @param EntityManager $em
+ * @param EntityManagerInterface $em
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(
PhoneVerificationOptions $options,
- EntityManager $em,
+ EntityManagerInterface $em,
EventDispatcherInterface $dispatcher
) {
$this->options = $options;
$this->em = $em;
$this->dispatcher = $dispatcher;
- $this->phoneVerificationRepository = $this->em
- ->getRepository('LoginCidadaoPhoneVerificationBundle:PhoneVerification');
- $this->sentVerificationRepository = $this->em
- ->getRepository('LoginCidadaoPhoneVerificationBundle:SentVerification');
+ $this->phoneVerificationRepository = $this->em->getRepository(PhoneVerification::class);
+ $this->sentVerificationRepository = $this->em->getRepository(SentVerification::class);
+ $this->personRepository = $this->em->getRepository(Person::class);
}
/**
@@ -297,6 +301,10 @@ public function sendVerificationCode(PhoneVerificationInterface $phoneVerificati
return $sentVerification;
}
+ /**
+ * @param SentVerificationInterface $sentVerification
+ * @return \LoginCidadao\PhoneVerificationBundle\Entity\SentVerification|SentVerificationInterface
+ */
public function registerVerificationSent(SentVerificationInterface $sentVerification)
{
$this->em->persist($sentVerification);
@@ -322,6 +330,11 @@ public function getNextResendDate(PhoneVerificationInterface $phoneVerification)
return $lastSentVerification->getSentAt()->add($timeout);
}
+ /**
+ * @param PhoneVerificationInterface $phoneVerification
+ * @param string $token
+ * @return bool
+ */
public function verifyToken(PhoneVerificationInterface $phoneVerification, $token)
{
if ($phoneVerification->isVerified()) {
@@ -334,4 +347,22 @@ public function verifyToken(PhoneVerificationInterface $phoneVerification, $toke
return $this->verify($phoneVerification, $phoneVerification->getVerificationCode());
}
+
+ /**
+ * @inheritDoc
+ */
+ public function isVerificationMandatory(PhoneVerificationInterface $phoneVerification): bool
+ {
+ $accountsCount = $this->personRepository->countByPhone($phoneVerification->getPhone());
+
+ return $accountsCount >= $this->options->getEnforceVerificationThreshold();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function countVerified(PhoneNumber $phoneNumber): int
+ {
+ return $this->phoneVerificationRepository->countVerified($phoneNumber);
+ }
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationServiceInterface.php b/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationServiceInterface.php
index f7298000c..11863e784 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationServiceInterface.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Service/PhoneVerificationServiceInterface.php
@@ -135,4 +135,18 @@ public function getNextResendDate(PhoneVerificationInterface $phoneVerification)
* @return bool
*/
public function verifyToken(PhoneVerificationInterface $phoneVerification, $providedToken);
+
+ /**
+ * Checks if the user MUST verify their phone
+ *
+ * @param PhoneVerificationInterface $phoneVerification
+ * @return bool
+ */
+ public function isVerificationMandatory(PhoneVerificationInterface $phoneVerification): bool;
+
+ /**
+ * @param PhoneNumber $phoneNumber
+ * @return int how many users have this PhoneNumber verified
+ */
+ public function countVerified(PhoneNumber $phoneNumber): int;
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/DependencyInjection/ConfigurationTest.php
index 7ab532859..a93390d35 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -20,6 +20,7 @@ public static function getSampleConfig()
{
return [
'enabled' => false,
+ 'require_validation_threshold' => 3,
'verification_code' => [
'length' => 6,
'use_numbers' => true,
@@ -33,6 +34,10 @@ public static function getSampleConfig()
'sms' => [
'resend_timeout' => '+5 minutes',
],
+ 'blocklist' => [
+ 'enable_auto_block' => true,
+ 'auto_block_limit' => 10,
+ ],
];
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/DependencyInjection/LoginCidadaoPhoneVerificationExtensionTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/DependencyInjection/LoginCidadaoPhoneVerificationExtensionTest.php
index 63abe2f04..40f84bdc2 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Tests/DependencyInjection/LoginCidadaoPhoneVerificationExtensionTest.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/DependencyInjection/LoginCidadaoPhoneVerificationExtensionTest.php
@@ -81,5 +81,9 @@ public function testParametersLoaded()
$config['verification_token']['length'],
$container->getParameter('lc.phone_verification.options.token.length')
);
+ $this->assertEquals(
+ $config['require_validation_threshold'],
+ $container->getParameter('lc.phone_verification.options.require_validation_threshold')
+ );
}
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Entity/BlockedPhoneNumberTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Entity/BlockedPhoneNumberTest.php
new file mode 100644
index 000000000..6ea0c8021
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Entity/BlockedPhoneNumberTest.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Tests\Entity;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\Person;
+use LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumber;
+use PHPUnit\Framework\TestCase;
+
+class BlockedPhoneNumberTest extends TestCase
+{
+ public function testEntity()
+ {
+ $phone = new PhoneNumber();
+ $blockedBy = new Person();
+ $date = new \DateTime();
+
+ $blocked = new BlockedPhoneNumber($phone, $blockedBy, $date);
+
+ $this->assertNull($blocked->getId());
+ $this->assertSame($phone, $blocked->getPhoneNumber());
+ $this->assertSame($blockedBy, $blocked->getBlockedBy());
+ $this->assertSame($date, $blocked->getCreatedAt());
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Event/BlocklistSubscriberTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Event/BlocklistSubscriberTest.php
new file mode 100644
index 000000000..e29d77553
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Event/BlocklistSubscriberTest.php
@@ -0,0 +1,139 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Tests\Event;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\PhoneVerificationBundle\Event\BlocklistSubscriber;
+use LoginCidadao\PhoneVerificationBundle\Event\PhoneChangedEvent;
+use LoginCidadao\PhoneVerificationBundle\PhoneVerificationEvents;
+use LoginCidadao\PhoneVerificationBundle\Service\BlocklistInterface;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
+use Symfony\Component\Security\Http\SecurityEvents;
+
+class BlocklistSubscriberTest extends TestCase
+{
+ public function testSubscribedEvents()
+ {
+ $this->assertEquals([
+ PhoneVerificationEvents::PHONE_CHANGED => 'onPhoneChange',
+ SecurityEvents::INTERACTIVE_LOGIN => 'onLogin',
+ ], BlocklistSubscriber::getSubscribedEvents());
+ }
+
+ public function testOnPhoneChange()
+ {
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+
+ $person = $this->createMock(PersonInterface::class);
+ $person->expects($this->once())->method('getMobile')->willReturn($phoneNumber);
+
+ /** @var PhoneChangedEvent|MockObject $event */
+ $event = $this->createMock(PhoneChangedEvent::class);
+ $event->expects($this->once())->method('getPerson')->willReturn($person);
+
+ $blocklistService = $this->getBlocklistService(true, $phoneNumber);
+
+ $subscriber = new BlocklistSubscriber($blocklistService, $this->getAuthorizationChecker());
+ $subscriber->onPhoneChange($event);
+ }
+
+ public function testOnLogin()
+ {
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+
+ $person = $this->createMock(PersonInterface::class);
+ $person->expects($this->once())->method('getMobile')->willReturn($phoneNumber);
+
+ $token = $this->createMock(TokenInterface::class);
+ $token->expects($this->once())->method('getUser')->willReturn($person);
+
+ /** @var InteractiveLoginEvent|MockObject $event */
+ $event = $this->createMock(InteractiveLoginEvent::class);
+ $event->expects($this->once())->method('getAuthenticationToken')->willReturn($token);
+
+ $blocklistService = $this->getBlocklistService(true, $phoneNumber);
+
+ $subscriber = new BlocklistSubscriber($blocklistService, $this->getAuthorizationChecker());
+ $subscriber->onLogin($event);
+ }
+
+ public function testDisabledFeatureOnPhoneChange()
+ {
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+
+ $person = $this->createMock(PersonInterface::class);
+ $person->expects($this->once())->method('getMobile')->willReturn($phoneNumber);
+
+ /** @var PhoneChangedEvent|MockObject $event */
+ $event = $this->createMock(PhoneChangedEvent::class);
+ $event->expects($this->once())->method('getPerson')->willReturn($person);
+
+ $blocklistService = $this->getBlocklistService(false);
+ $blocklistService->expects($this->never())->method('checkPhoneNumber');
+
+ $subscriber = new BlocklistSubscriber($blocklistService, $this->getAuthorizationChecker(false));
+ $subscriber->onPhoneChange($event);
+ }
+
+ public function testDisabledFeatureOnLogin()
+ {
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+
+ $person = $this->createMock(PersonInterface::class);
+ $person->expects($this->once())->method('getMobile')->willReturn($phoneNumber);
+
+ $token = $this->createMock(TokenInterface::class);
+ $token->expects($this->once())->method('getUser')->willReturn($person);
+
+ /** @var InteractiveLoginEvent|MockObject $event */
+ $event = $this->createMock(InteractiveLoginEvent::class);
+ $event->expects($this->once())->method('getAuthenticationToken')->willReturn($token);
+
+ $blocklistService = $this->getBlocklistService(false);
+
+ $subscriber = new BlocklistSubscriber($blocklistService, $this->getAuthorizationChecker(false));
+ $subscriber->onLogin($event);
+ }
+
+ /**
+ * @param bool $expectCheck
+ * @param PhoneNumber|null $phoneNumber
+ * @return MockObject|BlocklistInterface
+ */
+ private function getBlocklistService(bool $expectCheck = false, PhoneNumber $phoneNumber = null)
+ {
+ $blocklistService = $this->createMock(BlocklistInterface::class);
+
+ if ($expectCheck) {
+ $blocklistService->expects($this->once())->method('checkPhoneNumber')->with($phoneNumber);
+ }
+
+ return $blocklistService;
+ }
+
+ /**
+ * @param bool $isGranted
+ * @return MockObject|AuthorizationCheckerInterface
+ */
+ private function getAuthorizationChecker(bool $isGranted = true)
+ {
+ $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class);
+ $authorizationChecker->expects($this->once())->method('isGranted')
+ ->with('FEATURE_PHONE_BLOCKLIST')->willReturn($isGranted);
+
+ return $authorizationChecker;
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Event/TaskSubscriberTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Event/TaskSubscriberTest.php
index a4305b708..2dc1fb86b 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Tests/Event/TaskSubscriberTest.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Event/TaskSubscriberTest.php
@@ -13,6 +13,7 @@
use libphonenumber\PhoneNumberType;
use LoginCidadao\PhoneVerificationBundle\Event\TaskSubscriber;
use LoginCidadao\PhoneVerificationBundle\Exception\VerificationNotSentException;
+use LoginCidadao\PhoneVerificationBundle\Model\ConfirmPhoneTask;
use LoginCidadao\TaskStackBundle\TaskStackEvents;
use PHPUnit\Framework\TestCase;
@@ -72,12 +73,19 @@ public function testOnGetTasks()
$tokenStorage = $this->getTokenStorage();
$phoneVerificationService = $this->getPhoneVerificationService();
- $phoneVerificationService->expects($this->once())->method('getAllPendingPhoneVerification')
+ $phoneVerificationService->expects($this->once())
+ ->method('getAllPendingPhoneVerification')
->willReturn([$phoneVerification]);
+ $phoneVerificationService->expects($this->once())
+ ->method('isVerificationMandatory')
+ ->willReturn(true);
$event = $this->getMockBuilder('LoginCidadao\TaskStackBundle\Event\GetTasksEvent')
->disableOriginalConstructor()->getMock();
- $event->expects($this->once())->method('addTaskIfStackEmpty');
+ $event->expects($this->once())->method('addTaskIfStackEmpty')
+ ->willReturnCallback(function (ConfirmPhoneTask $task) {
+ $this->assertTrue($task->isMandatory());
+ });
$subscriber = new TaskSubscriber($tokenStorage, $phoneVerificationService, true);
$subscriber->onGetTasks($event);
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/BlockPhoneFormTypeTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/BlockPhoneFormTypeTest.php
new file mode 100644
index 000000000..df18325c7
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/BlockPhoneFormTypeTest.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Tests\Form;
+
+use libphonenumber\PhoneNumberFormat;
+use LoginCidadao\PhoneVerificationBundle\Form\BlockPhoneFormType;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockPhoneNumberRequest;
+use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class BlockPhoneFormTypeTest extends TestCase
+{
+ public function testConfigureOptions()
+ {
+ /** @var MockObject|OptionsResolver $resolver */
+ $resolver = $this->createMock(OptionsResolver::class);
+ $resolver->expects($this->once())->method('setDefaults')->with([
+ 'data_class' => BlockPhoneNumberRequest::class,
+ ]);
+
+ $form = new BlockPhoneFormType();
+ $form->configureOptions($resolver);
+ }
+
+ public function testBuildForm()
+ {
+ /** @var MockObject|FormBuilderInterface $builder */
+ $builder = $this->createMock(FormBuilderInterface::class);
+ $builder->expects($this->exactly(2))
+ ->method('add')
+ ->willReturnCallback(function ($name, $type, $options) use ($builder) {
+ switch ($name) {
+ case 'phoneNumber':
+ $this->checkPhoneNumberField($type, $options);
+
+ return $builder;
+ case 'submit':
+ $this->checkSubmitField($type, $options);
+
+ return $builder;
+ default:
+ $this->fail("Unexpected call to add() with name '{$name}'");
+
+ return null;
+ }
+ });
+
+ $form = new BlockPhoneFormType();
+ $form->buildForm($builder, []);
+ }
+
+ private function checkPhoneNumberField(string $type, array $options)
+ {
+ $this->assertEquals(PhoneNumberType::class, $type);
+ $this->assertTrue($options['required']);
+ $this->assertEquals('admin.blocklist.new.form.phone.label', $options['label']);
+ $this->assertStringContainsString('intl-tel', $options['attr']['class']);
+ $this->assertEquals('admin.blocklist.new.form.phone.placeholder', $options['attr']['placeholder']);
+ $this->assertEquals(PhoneNumberFormat::E164, $options['format']);
+ }
+
+ private function checkSubmitField(string $type, array $options)
+ {
+ $this->assertEquals(SubmitType::class, $type);
+ $this->assertEquals('admin.blocklist.new.form.submit.label', $options['label']);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/PhoneNumberFormTypeTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/PhoneNumberFormTypeTest.php
new file mode 100644
index 000000000..2244a6836
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/PhoneNumberFormTypeTest.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Tests\Form;
+
+use libphonenumber\PhoneNumberFormat;
+use LoginCidadao\CoreBundle\Entity\Person;
+use LoginCidadao\PhoneVerificationBundle\Form\PhoneNumberFormType;
+use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class PhoneNumberFormTypeTest extends TestCase
+{
+ public function testForm()
+ {
+ /** @var MockObject|FormBuilderInterface $builder */
+ $builder = $this->createMock(FormBuilderInterface::class);
+ $builder->expects($this->once())->method('add')->with(
+ $this->equalTo('mobile'),
+ $this->equalTo(PhoneNumberType::class),
+ $this->equalTo([
+ 'required' => false,
+ 'label' => 'person.form.mobile.label',
+ 'attr' => [
+ 'class' => 'form-control intl-tel',
+ 'placeholder' => 'person.form.mobile.placeholder',
+ ],
+ 'label_attr' => ['class' => 'intl-tel-label'],
+ 'format' => PhoneNumberFormat::E164,
+ ])
+ );
+
+ /** @var MockObject|OptionsResolver $resolver */
+ $resolver = $this->createMock(OptionsResolver::class);
+ $resolver->expects($this->once())->method('setDefaults')->with([
+ 'data_class' => Person::class,
+ 'validation_groups' => ['LoginCidadaoEmailForm'],
+ ]);
+
+ $form = new PhoneNumberFormType();
+ $form->configureOptions($resolver);
+ $form->buildForm($builder, []);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/SearchPhoneNumberTypeTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/SearchPhoneNumberTypeTest.php
new file mode 100644
index 000000000..0391c4bd6
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Form/SearchPhoneNumberTypeTest.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Tests\Form;
+
+use LoginCidadao\PhoneVerificationBundle\Form\SearchPhoneNumberType;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\Extension\Core\Type\TelType;
+use Symfony\Component\Form\FormBuilderInterface;
+
+class SearchPhoneNumberTypeTest extends TestCase
+{
+ public function testBuildForm()
+ {
+ /** @var MockObject|FormBuilderInterface $builder */
+ $builder = $this->createMock(FormBuilderInterface::class);
+ $builder->expects($this->exactly(2))
+ ->method('add')
+ ->willReturnCallback(function ($name, $type, $options) use ($builder) {
+ switch ($name) {
+ case 'phone':
+ $this->checkPhoneSearchField($type, $options);
+
+ return $builder;
+ case 'submit':
+ $this->checkSubmitField($type, $options);
+
+ return $builder;
+ default:
+ $this->fail("Unexpected call to add() with name '{$name}'");
+
+ return null;
+ }
+ });
+
+ $form = new SearchPhoneNumberType();
+ $form->buildForm($builder, []);
+ }
+
+ private function checkPhoneSearchField(string $type, array $options)
+ {
+ $this->assertEquals(TelType::class, $type);
+ $this->assertTrue($options['required']);
+ $this->assertEquals('admin.blocklist.list.form.phone.label', $options['label']);
+ $this->assertEquals('[0-9+]*', $options['attr']['pattern']);
+ $this->assertEquals('off', $options['attr']['autocomplete']);
+ $this->assertEquals('admin.blocklist.list.form.phone.placeholder', $options['attr']['placeholder']);
+ }
+
+ private function checkSubmitField(string $type, array $options)
+ {
+ $this->assertEquals(SubmitType::class, $type);
+ $this->assertEquals('admin.blocklist.list.form.submit.label', $options['label']);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Model/BlockPhoneNumberRequestTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Model/BlockPhoneNumberRequestTest.php
new file mode 100644
index 000000000..aa5595828
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Model/BlockPhoneNumberRequestTest.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Tests\Model;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockPhoneNumberRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class BlockPhoneNumberRequestTest extends TestCase
+{
+ public function testRequest()
+ {
+ /** @var MockObject|PersonInterface $person */
+ $person = $this->createMock(PersonInterface::class);
+
+ /** @var MockObject|PhoneNumber $phoneNumber */
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+
+ $request = new BlockPhoneNumberRequest($person);
+ $this->assertSame($person, $request->getBlockedBy());
+ $this->assertNull($request->phoneNumber);
+
+ $request->phoneNumber = $phoneNumber;
+
+ $this->assertSame($phoneNumber, $request->phoneNumber);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/BlocklistOptionsTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/BlocklistOptionsTest.php
new file mode 100644
index 000000000..ac81b0b6c
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/BlocklistOptionsTest.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Tests\Service;
+
+use LoginCidadao\PhoneVerificationBundle\Service\BlocklistOptions;
+use PHPUnit\Framework\TestCase;
+
+class BlocklistOptionsTest extends TestCase
+{
+ public function testOptions()
+ {
+ $autoBlockPhoneLimit = 3;
+
+ $options = new BlocklistOptions($autoBlockPhoneLimit);
+
+ $this->assertTrue($options->isAutoBlockEnabled());
+ $this->assertSame($autoBlockPhoneLimit, $options->getAutoBlockPhoneLimit());
+
+ $this->assertFalse((new BlocklistOptions(0))->isAutoBlockEnabled());
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/BlocklistTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/BlocklistTest.php
new file mode 100644
index 000000000..0aeebe875
--- /dev/null
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/BlocklistTest.php
@@ -0,0 +1,226 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\PhoneVerificationBundle\Tests\Service;
+
+use Doctrine\ORM\EntityManagerInterface;
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\Person;
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Mailer\TwigSwiftMailer;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\CoreBundle\Security\User\Manager\UserManager;
+use LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumber;
+use LoginCidadao\PhoneVerificationBundle\Entity\BlockedPhoneNumberRepository;
+use LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerification;
+use LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerificationRepository;
+use LoginCidadao\PhoneVerificationBundle\Model\BlockedPhoneNumberInterface;
+use LoginCidadao\PhoneVerificationBundle\Service\Blocklist;
+use LoginCidadao\PhoneVerificationBundle\Service\BlocklistOptions;
+use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationServiceInterface;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class BlocklistTest extends TestCase
+{
+ public function testManuallyBlocked()
+ {
+ $phoneNumber = new PhoneNumber();
+ $blocked = $this->createMock(BlockedPhoneNumberInterface::class);
+
+ $blockedPhoneRepository = $this->createMock(BlockedPhoneNumberRepository::class);
+ $blockedPhoneRepository->expects($this->once())
+ ->method('findByPhone')->with($phoneNumber)
+ ->willReturn($blocked);
+
+ $em = $this->getEntityManager($blockedPhoneRepository);
+
+ $options = new BlocklistOptions(2);
+
+ $blocklist = new Blocklist($this->getUserManager(), $this->getMailer(), $em,
+ $this->getPhoneVerificationService(), $options);
+ $this->assertTrue($blocklist->isPhoneBlocked($phoneNumber));
+ }
+
+ public function testAutoBlocked()
+ {
+ $phoneNumber = new PhoneNumber();
+
+ $blockedPhoneRepository = $this->createMock(BlockedPhoneNumberRepository::class);
+ $blockedPhoneRepository->expects($this->once())
+ ->method('findByPhone')->with($phoneNumber)
+ ->willReturn(null);
+
+ $phoneVerification = $this->getPhoneVerificationService();
+ $phoneVerification->expects($this->once())->method('countVerified')->willReturn(3);
+
+ $em = $this->getEntityManager($blockedPhoneRepository);
+
+ $options = new BlocklistOptions(2);
+
+ $blocklist = new Blocklist($this->getUserManager(), $this->getMailer(), $em, $phoneVerification, $options);
+ $this->assertTrue($blocklist->isPhoneBlocked($phoneNumber));
+ }
+
+ public function testNotBlocked()
+ {
+ $phoneNumber = new PhoneNumber();
+
+ $blockedPhoneRepository = $this->createMock(BlockedPhoneNumberRepository::class);
+ $blockedPhoneRepository->expects($this->once())
+ ->method('findByPhone')->with($phoneNumber)
+ ->willReturn(null);
+
+ $em = $this->getEntityManager($blockedPhoneRepository);
+
+ $options = new BlocklistOptions(0);
+
+ $blocklist = new Blocklist($this->getUserManager(), $this->getMailer(), $em,
+ $this->getPhoneVerificationService(), $options);
+ $this->assertFalse($blocklist->isPhoneBlocked($phoneNumber));
+ }
+
+ public function testBlockByPhone()
+ {
+ $phoneNumber = new PhoneNumber();
+ $users = [
+ (new Person())->setMobile($phoneNumber),
+ (new Person())->setMobile($phoneNumber),
+ (new Person())->setMobile($phoneNumber),
+ ];
+
+ $userManager = $this->getUserManager();
+ $userManager->expects($this->once())->method('blockUsersByPhone')
+ ->with($phoneNumber)->willReturn($users);
+
+ $mailer = $this->getMailer();
+ $mailer->expects($this->exactly(count($users)))->method('sendAccountAutoBlockedMessage')
+ ->with($this->isInstanceOf(PersonInterface::class));
+
+ $blockedPhoneRepository = $this->createMock(BlockedPhoneNumberRepository::class);
+
+ $em = $this->getEntityManager($blockedPhoneRepository);
+ $em->expects($this->once())->method('flush');
+
+ $options = new BlocklistOptions(0);
+
+ $blocklist = new Blocklist($userManager, $mailer, $em, $this->getPhoneVerificationService(), $options);
+ $this->assertSame($users, $blocklist->blockByPhone($phoneNumber));
+ }
+
+ public function testAddBlockedPhoneNumber()
+ {
+ /** @var MockObject|PhoneNumber $phoneNumber */
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+
+ /** @var MockObject|PersonInterface $blocker */
+ $blocker = $this->createMock(PersonInterface::class);
+
+ $userManager = $this->getUserManager();
+ $options = new BlocklistOptions(0);
+
+ $blockedPhoneRepository = $this->createMock(BlockedPhoneNumberRepository::class);
+ $em = $this->getEntityManager($blockedPhoneRepository);
+ $em->expects($this->once())->method('flush');
+ $em->expects($this->once())->method('persist')
+ ->with($this->isInstanceOf(BlockedPhoneNumberInterface::class));
+
+ $service = new Blocklist($userManager, $this->getMailer(), $em, $this->getPhoneVerificationService(), $options);
+ $blockedPhoneNumber = $service->addBlockedPhoneNumber($phoneNumber, $blocker);
+
+ $this->assertInstanceOf(BlockedPhoneNumberInterface::class, $blockedPhoneNumber);
+ $this->assertInstanceOf(\DateTime::class, $blockedPhoneNumber->getCreatedAt());
+ $this->assertSame($phoneNumber, $blockedPhoneNumber->getPhoneNumber());
+ $this->assertSame($blocker, $blockedPhoneNumber->getBlockedBy());
+ }
+
+ public function testCheckPhoneNumber()
+ {
+ $users = [
+ $this->createMock(PersonInterface::class),
+ $this->createMock(PersonInterface::class),
+ ];
+
+ /** @var PhoneNumber|MockObject $phoneNumber */
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+ $blockedPhone = $this->createMock(BlockedPhoneNumberInterface::class);
+
+ $blockedPhoneRepository = $this->createMock(BlockedPhoneNumberRepository::class);
+ $blockedPhoneRepository->expects($this->once())->method('findByPhone')->willReturn($blockedPhone);
+
+ $userManager = $this->getUserManager();
+ $userManager->expects($this->once())->method('blockUsersByPhone')->with($phoneNumber)->willReturn($users);
+
+ $mailer = $this->getMailer();
+ $mailer->expects($this->exactly(count($users)))->method('sendAccountAutoBlockedMessage')
+ ->with($this->isInstanceOf(PersonInterface::class));
+
+ $em = $this->getEntityManager($blockedPhoneRepository);
+ $options = new BlocklistOptions(0);
+
+ $service = new Blocklist($userManager, $mailer, $em, $this->getPhoneVerificationService(), $options);
+ $service->checkPhoneNumber($phoneNumber);
+ }
+
+ public function testGetBlockedPhoneNumberByPhone()
+ {
+ /** @var PhoneNumber|MockObject $phoneNumber */
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+ $blockedPhone = $this->createMock(BlockedPhoneNumberInterface::class);
+
+ $blockedPhoneRepository = $this->createMock(BlockedPhoneNumberRepository::class);
+ $blockedPhoneRepository->expects($this->once())->method('findByPhone')->willReturn($blockedPhone);
+
+ $em = $this->getEntityManager($blockedPhoneRepository);
+ $options = new BlocklistOptions(0);
+
+ $service = new Blocklist($this->getUserManager(), $this->getMailer(), $em,
+ $this->getPhoneVerificationService(), $options);
+ $this->assertSame($blockedPhone, $service->getBlockedPhoneNumberByPhone($phoneNumber));
+ }
+
+ /**
+ * @return MockObject|UserManager
+ */
+ private function getUserManager()
+ {
+ return $this->createMock(UserManager::class);
+ }
+
+ /**
+ * @return MockObject|TwigSwiftMailer
+ */
+ private function getMailer()
+ {
+ return $this->createMock(TwigSwiftMailer::class);
+ }
+
+ /**
+ * @param BlockedPhoneNumberRepository|MockObject $repository
+ * @return MockObject|EntityManagerInterface
+ */
+ private function getEntityManager($repository)
+ {
+ $em = $this->createMock(EntityManagerInterface::class);
+ $em->expects($this->once())->method('getRepository')
+ ->with(BlockedPhoneNumber::class)
+ ->willReturn($repository);
+
+ return $em;
+ }
+
+ /**
+ * @return MockObject|PhoneVerificationServiceInterface
+ */
+ private function getPhoneVerificationService()
+ {
+ return $this->createMock(PhoneVerificationServiceInterface::class);
+ }
+}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/PhoneVerificationOptionsTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/PhoneVerificationOptionsTest.php
index a91dc66ee..f1d680356 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/PhoneVerificationOptionsTest.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/PhoneVerificationOptionsTest.php
@@ -24,6 +24,7 @@ public function testOptions()
$caseSensitive = true;
$smsResendTimeout = '+10 minutes';
$tokenLength = 5;
+ $enforceVerificationThreshold = 2;
$options = new PhoneVerificationOptions(
$length,
@@ -32,7 +33,8 @@ public function testOptions()
$useLower,
$useUpper,
$smsResendTimeout,
- $tokenLength
+ $tokenLength,
+ $enforceVerificationThreshold
);
$this->assertEquals($length, $options->getLength());
@@ -42,5 +44,6 @@ public function testOptions()
$this->assertTrue($options->isCaseSensitive());
$this->assertEquals($smsResendTimeout, $options->getSmsResendTimeout());
$this->assertEquals($tokenLength, $options->getVerificationTokenLength());
+ $this->assertEquals($enforceVerificationThreshold, $options->getEnforceVerificationThreshold());
}
}
diff --git a/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/PhoneVerificationServiceTest.php b/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/PhoneVerificationServiceTest.php
index f26cfd589..dd48fc700 100644
--- a/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/PhoneVerificationServiceTest.php
+++ b/src/LoginCidadao/PhoneVerificationBundle/Tests/Service/PhoneVerificationServiceTest.php
@@ -10,41 +10,63 @@
namespace LoginCidadao\PhoneVerificationBundle\Tests\Service;
+use Doctrine\ORM\EntityManagerInterface;
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\Person;
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerification;
+use LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerificationRepository;
+use LoginCidadao\PhoneVerificationBundle\Entity\SentVerification;
+use LoginCidadao\PhoneVerificationBundle\Entity\SentVerificationRepository;
+use LoginCidadao\PhoneVerificationBundle\Event\SendPhoneVerificationEvent;
+use LoginCidadao\PhoneVerificationBundle\Exception\VerificationNotSentException;
+use LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface;
+use LoginCidadao\PhoneVerificationBundle\Model\SentVerificationInterface;
use LoginCidadao\PhoneVerificationBundle\PhoneVerificationEvents;
+use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationOptions;
use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationService;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
class PhoneVerificationServiceTest extends TestCase
{
+ /**
+ * @return MockObject|PhoneVerificationInterface
+ */
private function getPhoneVerification()
{
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
-
- return $phoneVerification;
+ return $this->createMock(PhoneVerificationInterface::class);
}
+ /**
+ * @return MockObject|PhoneVerificationOptions
+ */
private function getServiceOptions()
{
- $options = $this->getMockBuilder('LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationOptions')
+ $options = $this->getMockBuilder(PhoneVerificationOptions::class)
->disableOriginalConstructor()
->getMock();
return $options;
}
+ /**
+ * @return MockObject|EntityManagerInterface
+ */
private function getEntityManager()
{
- return $this->getMockBuilder('Doctrine\ORM\EntityManager')
- ->disableOriginalConstructor()
- ->getMock();
+ return $this->createMock(EntityManagerInterface::class);
}
+ /**
+ * @return MockObject|EventDispatcherInterface
+ */
private function getDispatcher()
{
- return $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')
- ->disableOriginalConstructor()
- ->getMock();
+ return $this->createMock(EventDispatcherInterface::class);
}
private function getRepository($class)
@@ -56,14 +78,28 @@ private function getRepository($class)
return $repository;
}
+ /**
+ * @return MockObject|PhoneVerificationRepository
+ */
private function getPhoneVerificationRepository()
{
- return $this->getRepository('LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerificationRepository');
+ return $this->getRepository(PhoneVerificationRepository::class);
}
+ /**
+ * @return MockObject|SentVerificationRepository
+ */
private function getSentVerificationRepository()
{
- return $this->getRepository('LoginCidadao\PhoneVerificationBundle\Entity\SentVerificationRepository');
+ return $this->getRepository(SentVerificationRepository::class);
+ }
+
+ /**
+ * @return MockObject|PersonRepository
+ */
+ private function getPersonRepository()
+ {
+ return $this->getRepository(PersonRepository::class);
}
/**
@@ -105,13 +141,21 @@ private function getService(array $arguments = [])
$sentVerificationRepository = $this->getSentVerificationRepository();
}
- $em->expects($this->exactly(2))->method('getRepository')->willReturnCallback(
- function ($class) use ($phoneVerificationRepository, $sentVerificationRepository) {
+ if (array_key_exists('person_repository', $arguments)) {
+ $personRepository = $arguments['person_repository'];
+ } else {
+ $personRepository = $this->getPersonRepository();
+ }
+
+ $em->expects($this->exactly(3))->method('getRepository')->willReturnCallback(
+ function ($class) use ($phoneVerificationRepository, $sentVerificationRepository, $personRepository) {
switch ($class) {
- case 'LoginCidadaoPhoneVerificationBundle:PhoneVerification':
+ case PhoneVerification::class:
return $phoneVerificationRepository;
- case 'LoginCidadaoPhoneVerificationBundle:SentVerification':
+ case SentVerification::class:
return $sentVerificationRepository;
+ case Person::class:
+ return $personRepository;
default:
return null;
}
@@ -123,24 +167,23 @@ function ($class) use ($phoneVerificationRepository, $sentVerificationRepository
public function testGetPhoneVerification()
{
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
+ $phoneVerification = $this->getPhoneVerification();
$repository = $this->getPhoneVerificationRepository();
$repository->expects($this->once())->method('findOneBy')->willReturn($phoneVerification);
$service = $this->getService(['phone_verification_repository' => $repository]);
- $person = $this->createMock('LoginCidadao\CoreBundle\Model\PersonInterface');
- $phone = $this->createMock('libphonenumber\PhoneNumber');
+ /** @var PersonInterface|MockObject $person */
+ $person = $this->createMock(PersonInterface::class);
+ $phone = $this->createMock(PhoneNumber::class);
$this->assertEquals($phoneVerification, $service->getPhoneVerification($person, $phone));
}
public function testCreatePhoneVerification()
{
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $existingPhoneVerification = $this->createMock($phoneVerificationClass);
+ $existingPhoneVerification = $this->getPhoneVerification();
$repository = $this->getPhoneVerificationRepository();
$repository->expects($this->atLeastOnce())->method('findOneBy')->willReturn($existingPhoneVerification);
@@ -152,19 +195,18 @@ public function testCreatePhoneVerification()
$service = $this->getService(['em' => $em, 'phone_verification_repository' => $repository]);
- $person = $this->createMock('LoginCidadao\CoreBundle\Model\PersonInterface');
- $phone = $this->createMock('libphonenumber\PhoneNumber');
+ /** @var PersonInterface|MockObject $person */
+ $person = $this->createMock(PersonInterface::class);
+ $phone = $this->createMock(PhoneNumber::class);
- $this->assertInstanceOf($phoneVerificationClass, $service->createPhoneVerification($person, $phone));
+ $this->assertInstanceOf(PhoneVerificationInterface::class, $service->createPhoneVerification($person, $phone));
}
public function testGetPendingPhoneVerification()
{
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
+ $phoneVerification = $this->getPhoneVerification();
- $repoClass = 'LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerificationRepository';
- $repository = $this->getMockBuilder($repoClass)
+ $repository = $this->getMockBuilder(PhoneVerificationRepository::class)
->disableOriginalConstructor()
->getMock();
$repository->expects($this->once())->method('findOneBy')->willReturn($phoneVerification);
@@ -172,8 +214,9 @@ public function testGetPendingPhoneVerification()
$service = $this->getService(['phone_verification_repository' => $repository]);
- $person = $this->createMock('LoginCidadao\CoreBundle\Model\PersonInterface');
- $phone = $this->createMock('libphonenumber\PhoneNumber');
+ /** @var PersonInterface|MockObject $person */
+ $person = $this->createMock(PersonInterface::class);
+ $phone = $this->createMock(PhoneNumber::class);
$this->assertEquals($phoneVerification, $service->getPendingPhoneVerification($person, $phone));
$this->assertEquals($phoneVerification, $service->getAllPendingPhoneVerification($person));
@@ -187,8 +230,7 @@ public function testRemovePhoneVerification()
$service = $this->getService(compact('em'));
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
+ $phoneVerification = $this->getPhoneVerification();
$this->assertTrue($service->removePhoneVerification($phoneVerification));
}
@@ -203,8 +245,9 @@ public function testEnforcePhoneVerification()
$service = $this->getService(['em' => $em, 'phone_verification_repository' => $repository]);
- $person = $this->createMock('LoginCidadao\CoreBundle\Model\PersonInterface');
- $phone = $this->createMock('libphonenumber\PhoneNumber');
+ /** @var PersonInterface|MockObject $person */
+ $person = $this->createMock(PersonInterface::class);
+ $phone = $this->createMock(PhoneNumber::class);
$service->enforcePhoneVerification($person, $phone);
}
@@ -233,8 +276,7 @@ public function testCheckNotCaseSensitiveVerificationCode()
public function testSuccessfulVerify()
{
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
+ $phoneVerification = $this->getPhoneVerification();
$phoneVerification->expects($this->once())->method('setVerifiedAt');
$phoneVerification->expects($this->atLeastOnce())->method('getVerificationCode')->willReturn('123');
@@ -253,8 +295,7 @@ public function testSuccessfulVerify()
public function testUnsuccessfulVerify()
{
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
+ $phoneVerification = $this->getPhoneVerification();
$phoneVerification->expects($this->never())->method('setVerifiedAt');
$phoneVerification->expects($this->atLeastOnce())->method('getVerificationCode')->willReturn('321');
@@ -274,9 +315,8 @@ public function testUnsuccessfulVerify()
public function testGetPhoneVerificationById()
{
$id = random_int(1, 9999);
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
- $person = $this->createMock('LoginCidadao\CoreBundle\Model\PersonInterface');
+ $phoneVerification = $this->createMock(PhoneVerificationInterface::class);
+ $person = $this->createMock(PersonInterface::class);
$repository = $this->getPhoneVerificationRepository();
$repository->expects($this->once())->method('findOneBy')
@@ -291,9 +331,8 @@ public function testGetPhoneVerificationById()
public function testGetPendingPhoneVerificationById()
{
$id = random_int(1, 9999);
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
- $person = $this->createMock('LoginCidadao\CoreBundle\Model\PersonInterface');
+ $phoneVerification = $this->createMock(PhoneVerificationInterface::class);
+ $person = $this->createMock(PersonInterface::class);
$repository = $this->getPhoneVerificationRepository();
$repository->expects($this->once())->method('findOneBy')
@@ -307,20 +346,18 @@ public function testGetPendingPhoneVerificationById()
public function testSendVerificationCodeSuccess()
{
- $phoneVerificationClass = 'LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface';
- $phoneVerification = $this->createMock($phoneVerificationClass);
+ $phoneVerification = $this->getPhoneVerification();
- $sentVerification = $this->createMock(
- 'LoginCidadao\PhoneVerificationBundle\Model\SentVerificationInterface'
- );
+ /** @var SentVerificationInterface|MockObject $sentVerification */
+ $sentVerification = $this->createMock(SentVerificationInterface::class);
$dispatcher = $this->getDispatcher();
$dispatcher->expects($this->once())->method('dispatch')
->with(
PhoneVerificationEvents::PHONE_VERIFICATION_REQUESTED,
- $this->isInstanceOf('LoginCidadao\PhoneVerificationBundle\Event\SendPhoneVerificationEvent')
+ $this->isInstanceOf(SendPhoneVerificationEvent::class)
)->willReturnCallback(
- function ($eventName, $event) use ($sentVerification) {
+ function ($eventName, SendPhoneVerificationEvent $event) use ($sentVerification) {
$event->setSentVerification($sentVerification);
}
);
@@ -333,7 +370,7 @@ function ($eventName, $event) use ($sentVerification) {
public function testSendVerificationCodeFailure()
{
- $this->expectException('LoginCidadao\PhoneVerificationBundle\Exception\VerificationNotSentException');
+ $this->expectException(VerificationNotSentException::class);
$phoneVerification = $this->getPhoneVerification();
@@ -341,7 +378,7 @@ public function testSendVerificationCodeFailure()
$dispatcher->expects($this->once())->method('dispatch')
->with(
PhoneVerificationEvents::PHONE_VERIFICATION_REQUESTED,
- $this->isInstanceOf('LoginCidadao\PhoneVerificationBundle\Event\SendPhoneVerificationEvent')
+ $this->isInstanceOf(SendPhoneVerificationEvent::class)
);
$service = $this->getService(compact('dispatcher'));
@@ -356,13 +393,11 @@ public function testResendVerificationCodeSuccess()
$dispatcher->expects($this->once())->method('dispatch')
->with(
PhoneVerificationEvents::PHONE_VERIFICATION_REQUESTED,
- $this->isInstanceOf('LoginCidadao\PhoneVerificationBundle\Event\SendPhoneVerificationEvent')
+ $this->isInstanceOf(SendPhoneVerificationEvent::class)
)->willReturnCallback(
- function ($eventName, $event) {
- $sentAt = new \DateTime("-5 minutes");
- $sentVerification = $this->createMock(
- 'LoginCidadao\PhoneVerificationBundle\Model\SentVerificationInterface'
- );
+ function ($eventName, SendPhoneVerificationEvent $event) {
+ /** @var SentVerificationInterface|MockObject $sentVerification */
+ $sentVerification = $this->createMock(SentVerificationInterface::class);
$event->setSentVerification($sentVerification);
}
);
@@ -373,9 +408,9 @@ function ($eventName, $event) {
public function testResendVerificationCodeFailure()
{
- $this->expectException('Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException');
+ $this->expectException(TooManyRequestsHttpException::class);
$sentAt = new \DateTime();
- $sentVerification = $this->createMock('LoginCidadao\PhoneVerificationBundle\Model\SentVerificationInterface');
+ $sentVerification = $this->createMock(SentVerificationInterface::class);
$sentVerification->expects($this->once())->method('getSentAt')->willReturn($sentAt);
$phoneVerification = $this->getPhoneVerification();
@@ -392,7 +427,8 @@ public function testResendVerificationCodeFailure()
public function testRegisterVerificationSent()
{
- $sentVerification = $this->createMock('LoginCidadao\PhoneVerificationBundle\Model\SentVerificationInterface');
+ /** @var SentVerificationInterface|MockObject $sentVerification */
+ $sentVerification = $this->createMock(SentVerificationInterface::class);
$em = $this->getEntityManager();
$em->expects($this->once())->method('persist')->with($sentVerification);
@@ -407,7 +443,7 @@ public function testGetNextResendDate()
$timeout = \DateInterval::createFromDateString('+ 5 minutes');
$sentAt = new \DateTime("-5 minutes");
$expected = $sentAt->add($timeout);
- $sentVerification = $this->createMock('LoginCidadao\PhoneVerificationBundle\Model\SentVerificationInterface');
+ $sentVerification = $this->createMock(SentVerificationInterface::class);
$sentVerification->expects($this->once())->method('getSentAt')->willReturn($sentAt);
$repository = $this->getSentVerificationRepository();
@@ -458,4 +494,51 @@ public function testVerifyTokenInvalidToken()
$service = $this->getService();
$this->assertFalse($service->verifyToken($phoneVerification, 'wrong'));
}
+
+ public function testMandatoryVerification()
+ {
+ $this->assertTrue($this->isMandatoryTest(3, 3));
+ $this->assertTrue($this->isMandatoryTest(4, 3));
+ }
+
+ public function testOptionalVerification()
+ {
+ $this->assertFalse($this->isMandatoryTest(2, 3));
+ $this->assertFalse($this->isMandatoryTest(0, 3));
+ }
+
+ public function testCountVerified()
+ {
+ $count = 5;
+
+ /** @var PhoneNumber|MockObject $phoneNumber */
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+
+ $repo = $this->getPhoneVerificationRepository();
+ $repo->expects($this->once())->method('countVerified')->with($phoneNumber)->willReturn($count);
+
+ $service = $this->getService(['phone_verification_repository' => $repo]);
+ $this->assertEquals($count, $service->countVerified($phoneNumber));
+ }
+
+ private function isMandatoryTest(int $phoneCount, int $threshold)
+ {
+ $phoneNumber = $this->createMock(PhoneNumber::class);
+
+ /** @var PhoneVerificationInterface|MockObject $phoneVerification */
+ $phoneVerification = $this->createMock(PhoneVerificationInterface::class);
+ $phoneVerification->expects($this->once())->method('getPhone')->willReturn($phoneNumber);
+
+ $personRepo = $this->getPersonRepository();
+ $personRepo->expects($this->once())->method('countByPhone')->with($phoneNumber)->willReturn($phoneCount);
+
+ $options = new PhoneVerificationOptions(6, true, true, false, false, 10, 6, $threshold);
+
+ $phoneVerificationService = $this->getService([
+ 'person_repository' => $personRepo,
+ 'options' => $options,
+ ]);
+
+ return $phoneVerificationService->isVerificationMandatory($phoneVerification);
+ }
}
diff --git a/src/LoginCidadao/SupportBundle/Controller/PersonSupportController.php b/src/LoginCidadao/SupportBundle/Controller/PersonSupportController.php
new file mode 100644
index 000000000..da7c3748b
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Controller/PersonSupportController.php
@@ -0,0 +1,143 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Controller;
+
+use Doctrine\ORM\NonUniqueResultException;
+use Doctrine\ORM\QueryBuilder;
+use LoginCidadao\APIBundle\Security\Audit\ActionLogger;
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Entity\SentEmail;
+use LoginCidadao\CoreBundle\Helper\GridHelper;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\SupportBundle\Form\PersonSearchFormType;
+use LoginCidadao\SupportBundle\Model\PersonSearchRequest;
+use LoginCidadao\SupportBundle\Service\SupportHandler;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Class PersonSupportController
+ * @package LoginCidadao\SupportBundle\Controller
+ *
+ * @Security("has_role('ROLE_SUPPORT_AGENT')")
+ * @codeCoverageIgnore
+ */
+class PersonSupportController extends Controller
+{
+ /**
+ * @Route("/support/search", name="lc_support_person_search")
+ * @Security("has_role('ROLE_SUPPORT_SEARCH_USERS')")
+ * @param Request $request
+ * @return Response
+ */
+ public function searchAction(Request $request)
+ {
+ $gridView = null;
+ $search = new PersonSearchRequest();
+
+ $search->smartSearch = $request->get('search', null);
+
+ $form = $this->createForm(PersonSearchFormType::class, $search);
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ /** @var PersonRepository $repo */
+ $repo = $this->getDoctrine()->getRepository('LoginCidadaoCoreBundle:Person');
+ $query = $repo->getSmartSearchQuery($search->smartSearch);
+ try {
+ $person = $query->getQuery()->getOneOrNullResult();
+
+ if ($person instanceof PersonInterface) {
+ return $this->redirectToRoute('lc_support_person_view', [
+ 'id' => $person->getId(),
+ 'ticket' => $search->supportTicket,
+ ]);
+ }
+ } catch (NonUniqueResultException $e) {
+ $grid = $this->getPersonGrid($query, $form);
+ $gridView = $grid->createView($request);
+ }
+ }
+
+ return $this->render('LoginCidadaoSupportBundle:PersonSupport:index.html.twig', [
+ 'form' => $form->createView(),
+ 'grid' => $gridView,
+ 'search' => $search,
+ ]);
+ }
+
+ /**
+ * @Route("/support/person/{id}", name="lc_support_person_view")
+ * @param Request $request
+ * @param $id
+ * @return Response
+ */
+ public function viewAction(Request $request, $id)
+ {
+ try {
+ $supportRequest = $this->validateSupportTicketId($request->get('ticket'));
+ } catch (NotFoundHttpException $e) {
+ if (!$this->isGranted('ROLE_SKIP_SUPPORT_TOKEN_VALIDATION')) {
+ throw $e;
+ }
+ $supportRequest = null;
+ }
+
+ /** @var SupportHandler $supportHandler */
+ $supportHandler = $this->get(SupportHandler::class);
+
+ $person = $supportHandler->getSupportPerson($id);
+ $phoneMetadata = $supportHandler->getPhoneMetadata($person);
+ $thirdPartyConnections = $supportHandler->getThirdPartyConnections($person);
+
+ /** @var ActionLogger $actionLogger */
+ $actionLogger = $this->get('lc.action_logger');
+ $actionLogger->registerProfileView($request, $person, $this->getUser(), [$this, 'viewAction']);
+
+ return $this->render('LoginCidadaoSupportBundle:PersonSupport:view.html.twig', [
+ 'person' => $person,
+ 'thirdPartyConnections' => $thirdPartyConnections,
+ 'supportRequest' => $supportRequest,
+ 'dataValidation' => $supportHandler->getValidationMap($person),
+ 'phoneMetadata' => $phoneMetadata,
+ ]);
+ }
+
+ private function validateSupportTicketId(string $ticket = null): SentEmail
+ {
+ /** @var SupportHandler $supportHandler */
+ $supportHandler = $this->get(SupportHandler::class);
+ $sentEmail = $ticket ? $supportHandler->getInitialMessage($ticket) : null;
+ if (!$sentEmail instanceof SentEmail) {
+ throw $this->createNotFoundException("Invalid Support Ticket ID");
+ }
+
+ return $sentEmail;
+ }
+
+ private function getPersonGrid(QueryBuilder $query, $form): GridHelper
+ {
+ $grid = new GridHelper();
+ $grid->setId('person-grid');
+ $grid->setPerPage(5);
+ $grid->setMaxResult(5);
+ $grid->setInfiniteGrid(true);
+ $grid->setRoute('lc_support_person_search');
+ $grid->setRouteParams([$form->getName()]);
+ $grid->setQueryBuilder($query);
+
+ return $grid;
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Exception/PersonNotFoundException.php b/src/LoginCidadao/SupportBundle/Exception/PersonNotFoundException.php
new file mode 100644
index 000000000..45995c8f9
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Exception/PersonNotFoundException.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Exception;
+
+class PersonNotFoundException extends \RuntimeException
+{
+ //
+}
diff --git a/src/LoginCidadao/SupportBundle/Form/PersonSearchFormType.php b/src/LoginCidadao/SupportBundle/Form/PersonSearchFormType.php
new file mode 100644
index 000000000..7b710bd0b
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Form/PersonSearchFormType.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Form;
+
+use LoginCidadao\SupportBundle\Model\PersonSearchRequest;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class PersonSearchFormType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->setMethod('GET')
+ ->add('supportTicket', TextType::class)
+ ->add('smartSearch', TextType::class, [
+ 'label' => 'security.login.username',
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'data_class' => PersonSearchRequest::class,
+ 'csrf_protection' => false,
+ ]);
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/LoginCidadaoSupportBundle.php b/src/LoginCidadao/SupportBundle/LoginCidadaoSupportBundle.php
new file mode 100644
index 000000000..862bd7d36
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/LoginCidadaoSupportBundle.php
@@ -0,0 +1,9 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Model;
+
+use Symfony\Component\Validator\Constraints as Assert;
+use LoginCidadao\ValidationBundle\Validator\Constraints as LCAssert;
+
+class PersonSearchRequest
+{
+ /**
+ * @Assert\NotBlank()
+ *
+ * @var string
+ */
+ public $supportTicket;
+
+ /**
+ * @Assert\NotBlank()
+ * @Assert\Type("string")
+ *
+ * @var string
+ */
+ public $smartSearch;
+
+ /**
+ * @var string
+ */
+ public $phoneNumber;
+
+ /**
+ * @Assert\Email(checkHost=false, checkMX=false, strict=false)
+ *
+ * @var string
+ */
+ public $email;
+
+ /**
+ * @LCAssert\CPF()
+ *
+ * @var string
+ */
+ public $cpf;
+}
diff --git a/src/LoginCidadao/SupportBundle/Model/PersonalData.php b/src/LoginCidadao/SupportBundle/Model/PersonalData.php
new file mode 100644
index 000000000..068c397cd
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Model/PersonalData.php
@@ -0,0 +1,149 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Model;
+
+class PersonalData
+{
+ const HASH_ALGO = 'sha512';
+
+ /** @var string */
+ private $name;
+
+ /** @var string */
+ private $hash;
+
+ /** @var string */
+ private $challenge;
+
+ /** @var string */
+ private $value;
+
+ /** @var bool */
+ private $isValueFilled;
+
+ /**
+ * PersonalData constructor.
+ * @param string $name
+ * @param string $hash
+ * @param string $value
+ * @param bool $isValueFilled
+ * @param string $challenge
+ */
+ public function __construct(
+ string $name,
+ string $value = null,
+ bool $isValueFilled = null,
+ string $hash = null,
+ string $challenge = null
+ ) {
+ $this->name = $name;
+ $this->value = $value;
+ $this->isValueFilled = $isValueFilled ?? (bool)$value;
+ $this->challenge = self::enforceChallenge($challenge);
+ $this->setHash($hash);
+ }
+
+ public static function createWithValue(string $name, ?string $value, string $challenge = null): PersonalData
+ {
+ if (null === $value) {
+ $value = '';
+ $filled = false;
+ } else {
+ $filled = (bool)$value;
+ }
+
+ return new self($name, $value, $filled, null, $challenge);
+ }
+
+ public static function createWithoutValue(string $name, ?string $value, string $challenge = null): PersonalData
+ {
+ $challenge = self::enforceChallenge($challenge);
+ $hash = self::generateHash(self::enforceChallenge($challenge), $value);
+
+ return new self($name, null, (bool)$value, $hash, $challenge);
+ }
+
+ public function checkValue(string $value): bool
+ {
+ $userHash = $this->generateHash($this->getChallenge(), $value);
+
+ return hash_equals($this->getHash(), $userHash);
+ }
+
+ private function setHash(string $hash = null)
+ {
+ if ($hash === null) {
+ if ($this->value !== null) {
+ $hash = $this->generateHash($this->getChallenge(), $this->getValue());
+ } else {
+ throw new \InvalidArgumentException("Hash and Value can't both be null");
+ }
+ }
+
+ $this->hash = $hash;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getHash(): string
+ {
+ return $this->hash;
+ }
+
+ /**
+ * @return string
+ */
+ public function getChallenge(): string
+ {
+ return $this->challenge;
+ }
+
+ /**
+ * @return string
+ */
+ public function getValue(): ?string
+ {
+ return $this->value;
+ }
+
+ public function isValueFilled(): bool
+ {
+ return $this->isValueFilled;
+ }
+
+ private static function generateHash(string $challenge, ?string $value): string
+ {
+ return hash_hmac(self::HASH_ALGO, $challenge, $value);
+ }
+
+ private static function enforceChallenge(string $challenge = null): string
+ {
+ return $challenge ?? bin2hex(random_bytes(10));
+ }
+
+ public function __toString(): string
+ {
+ if ($this->getValue() !== null) {
+ return $this->getValue();
+ } else {
+ return $this->isValueFilled ? 'Yes' : 'No';
+ }
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Model/SupportPerson.php b/src/LoginCidadao/SupportBundle/Model/SupportPerson.php
new file mode 100644
index 000000000..9c6914a45
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Model/SupportPerson.php
@@ -0,0 +1,237 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Model;
+
+use libphonenumber\PhoneNumber;
+use libphonenumber\PhoneNumberFormat;
+use libphonenumber\PhoneNumberUtil;
+use LoginCidadao\CoreBundle\Model\IdentifiablePersonInterface;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+
+class SupportPerson implements IdentifiablePersonInterface
+{
+ /** @var mixed */
+ private $id;
+
+ /** @var string */
+ private $firstName;
+
+ /** @var string */
+ private $lastName;
+
+ /** @var PersonalData */
+ private $birthday;
+
+ /** @var PersonalData */
+ private $cpf;
+
+ /** @var PersonalData */
+ private $email;
+
+ /** @var \DateTimeInterface */
+ private $emailVerifiedAt;
+
+ /** @var PersonalData */
+ private $phoneNumber;
+
+ /** @var \DateTimeInterface */
+ private $lastPasswordResetRequest;
+
+ /** @var bool */
+ private $has2FA;
+
+ /** @var bool */
+ private $isEnabled;
+
+ /** @var \DateTimeInterface */
+ private $lastUpdate;
+
+ /** @var \DateTimeInterface */
+ private $createdAt;
+
+ /** @var \DateTimeInterface */
+ private $lastLogin;
+
+ /**
+ * SupportPerson constructor.
+ * @param PersonInterface $person
+ * @param AuthorizationCheckerInterface $authorizationChecker
+ */
+ public function __construct(PersonInterface $person, AuthorizationCheckerInterface $authorizationChecker)
+ {
+ $this->id = $person->getId();
+ $this->firstName = $person->getFirstName();
+ $this->lastName = $person->getSurname();
+ $this->emailVerifiedAt = $person->getEmailConfirmedAt();
+ $this->lastPasswordResetRequest = $person->getPasswordRequestedAt();
+ $this->has2FA = $person->getGoogleAuthenticatorSecret() !== null;
+ $this->isEnabled = $person->isEnabled();
+ $this->lastUpdate = $person->getUpdatedAt();
+ $this->createdAt = $person->getCreatedAt();
+ $this->lastLogin = $person->getLastLogin();
+
+ if ($authorizationChecker->isGranted('ROLE_VIEW_USERS_CPF')) {
+ $this->cpf = PersonalData::createWithValue('cpf', $person->getCpf());
+ } else {
+ $this->cpf = PersonalData::createWithoutValue('cpf', $person->getCpf());
+ }
+ if ($authorizationChecker->isGranted('ROLE_SUPPORT_VIEW_EMAIL')) {
+ $this->email = PersonalData::createWithValue('email', $person->getEmailCanonical());
+ } else {
+ $this->email = PersonalData::createWithoutValue('email', $person->getEmailCanonical());
+ }
+ $this->setPhoneNumber($person, $authorizationChecker);
+ $this->setBirthday($person, $authorizationChecker);
+ }
+
+ private function setPhoneNumber(PersonInterface $person, AuthorizationCheckerInterface $authorizationChecker)
+ {
+ $phoneNumber = $person->getMobile();
+ if ($phoneNumber instanceof PhoneNumber) {
+ $phoneUtil = PhoneNumberUtil::getInstance();
+ $phoneNumber = $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
+ }
+ if ($authorizationChecker->isGranted('ROLE_SUPPORT_VIEW_PHONE')) {
+ $this->phoneNumber = PersonalData::createWithValue('phoneNumber', $phoneNumber);
+ } else {
+ $this->phoneNumber = PersonalData::createWithoutValue('phoneNumber', $phoneNumber);
+ }
+ }
+
+ private function setBirthday(PersonInterface $person, AuthorizationCheckerInterface $authorizationChecker)
+ {
+ $birthday = $person->getBirthdate();
+ if ($birthday instanceof \DateTimeInterface) {
+ $birthday = $birthday->format('Y-m-d');
+ }
+
+ if ($authorizationChecker->isGranted('ROLE_SUPPORT_VIEW_BIRTHDAY')) {
+ $this->birthday = PersonalData::createWithValue('birthday', $birthday);
+ } else {
+ $this->birthday = PersonalData::createWithoutValue('birthday', $birthday);
+ }
+ }
+
+ public function getName(): string
+ {
+ return $this->getFirstName() ?? $this->getId();
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFirstName(): ?string
+ {
+ return $this->firstName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLastName(): ?string
+ {
+ return $this->lastName;
+ }
+
+ /**
+ * @return PersonalData
+ */
+ public function getBirthday(): PersonalData
+ {
+ return $this->birthday;
+ }
+
+ /**
+ * @return PersonalData
+ */
+ public function getCpf(): PersonalData
+ {
+ return $this->cpf;
+ }
+
+ /**
+ * @return PersonalData
+ */
+ public function getEmail(): PersonalData
+ {
+ return $this->email;
+ }
+
+ /**
+ * @return \DateTimeInterface
+ */
+ public function getEmailVerifiedAt(): ?\DateTimeInterface
+ {
+ return $this->emailVerifiedAt;
+ }
+
+ /**
+ * @return PersonalData
+ */
+ public function getPhoneNumber(): PersonalData
+ {
+ return $this->phoneNumber;
+ }
+
+ /**
+ * @return \DateTimeInterface
+ */
+ public function getLastPasswordResetRequest(): ?\DateTimeInterface
+ {
+ return $this->lastPasswordResetRequest;
+ }
+
+ /**
+ * @return bool
+ */
+ public function has2FA(): bool
+ {
+ return $this->has2FA;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return $this->isEnabled;
+ }
+
+ /**
+ * @return \DateTimeInterface
+ */
+ public function getLastUpdate(): \DateTimeInterface
+ {
+ return $this->lastUpdate;
+ }
+
+ /**
+ * @return \DateTimeInterface
+ */
+ public function getCreatedAt(): \DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ /**
+ * @return \DateTimeInterface
+ */
+ public function getLastLogin(): \DateTimeInterface
+ {
+ return $this->lastLogin;
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Resources/config/services.yml b/src/LoginCidadao/SupportBundle/Resources/config/services.yml
new file mode 100644
index 000000000..ab6046c97
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/config/services.yml
@@ -0,0 +1,13 @@
+services:
+ LoginCidadao\SupportBundle\Service\SupportHandler:
+ class: LoginCidadao\SupportBundle\Service\SupportHandler
+ arguments:
+ - "@security.authorization_checker"
+ - "@phone_verification"
+ - "@lc.person.repository"
+ - "@lc.sent_email.repository"
+
+ LoginCidadao\SupportBundle\Twig\SupportExtensions:
+ clas: LoginCidadao\SupportBundle\Twig\SupportExtensions
+ tags:
+ - { name: 'twig.extension' }
diff --git a/src/LoginCidadao/SupportBundle/Resources/public/js/sha512.js b/src/LoginCidadao/SupportBundle/Resources/public/js/sha512.js
new file mode 100644
index 000000000..1c72e94a3
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/public/js/sha512.js
@@ -0,0 +1,38 @@
+/*
+ Downloaded from: https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.3.1/sha512.js
+
+ A JavaScript implementation of the SHA family of hashes, as
+ defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding
+ HMAC implementation as defined in FIPS PUB 198a
+
+ Copyright Brian Turek 2008-2017
+ Distributed under the BSD License
+ See http://caligatio.github.com/jsSHA/ for more information
+
+ Several functions taken from Paul Johnston
+*/
+'use strict';(function(K){function x(b,a,d){var e=0,g=[],k=0,f,c,m,h,l,p,t,q,y=!1,u=[],r=[],v,A=!1;d=d||{};f=d.encoding||"UTF8";v=d.numRounds||1;if(v!==parseInt(v,10)||1>v)throw Error("numRounds must a integer >= 1");if(0===b.lastIndexOf("SHA-",0))if(p=function(a,d){return B(a,d,b)},t=function(a,d,g,e){var c,k;if("SHA-384"===b||"SHA-512"===b)c=(d+129>>>10<<5)+31,k=32;else throw Error("Unexpected error in SHA-2 implementation");for(;a.length<=c;)a.push(0);a[d>>>5]|=128<<24-d%32;d=d+g;a[c]=d&4294967295;
+ a[c-1]=d/4294967296|0;g=a.length;for(d=0;d
>>3;g=k/4-1;if(ka/8){for(;d.length<=g;)d.push(0);d[g]&=4294967040}for(a=0;a<=g;a+=1)u[a]=d[a]^909522486,r[a]=d[a]^1549556828;c=p(u,c);e=l;y=
+ !0};this.update=function(a){var d,b,n,f=0,h=l>>>5;d=m(a,g,k);a=d.binLen;b=d.value;d=a>>>5;for(n=0;n>>5);k=a%l;A=!0};this.getHash=function(a,d){var f,l,m,p;if(!0===y)throw Error("Cannot call getHash after setting HMAC key");m=D(d);switch(a){case "HEX":f=function(a){return E(a,h,m)};break;case "B64":f=function(a){return F(a,h,m)};break;case "BYTES":f=function(a){return G(a,h)};break;case "ARRAYBUFFER":try{l=new ArrayBuffer(0)}catch(w){throw Error("ARRAYBUFFER not supported by this environment");
+}f=function(a){return H(a,h)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER");}p=t(g.slice(),k,e,q(c));for(l=1;l>>2]>>>8*(3+g%4*-1),e+="0123456789abcdef".charAt(k>>>4&15)+"0123456789abcdef".charAt(k&15);return d.outputUpper?e.toUpperCase():e}function F(b,a,d){var e="",g=a/8,k,f,c;for(k=0;k>>2]:0,c=k+2>>2]:
+ 0,c=(b[k>>>2]>>>8*(3+k%4*-1)&255)<<16|(f>>>8*(3+(k+1)%4*-1)&255)<<8|c>>>8*(3+(k+2)%4*-1)&255,f=0;4>f;f+=1)8*k+6*f<=a?e+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(c>>>6*(3-f)&63):e+=d.b64Pad;return e}function G(b,a){var d="",e=a/8,g,c;for(g=0;g>>2]>>>8*(3+g%4*-1)&255,d+=String.fromCharCode(c);return d}function H(b,a){var d=a/8,e,g=new ArrayBuffer(d),c;c=new Uint8Array(g);for(e=0;e>>2]>>>8*(3+e%4*-1)&255;return g}function D(b){var a={outputUpper:!1,
+ b64Pad:"=",shakeLen:-1};b=b||{};a.outputUpper=b.outputUpper||!1;!0===b.hasOwnProperty("b64Pad")&&(a.b64Pad=b.b64Pad);if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option");if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function C(b,a){var d;switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE");}switch(b){case "HEX":d=function(a,b,d){var f=a.length,c,n,h,l,
+ p;if(0!==f%2)throw Error("String of HEX type must be in byte increments");b=b||[0];d=d||0;p=d>>>3;for(c=0;c>>1)+p;for(h=l>>>2;b.length<=h;)b.push(0);b[h]|=n<<8*(3+l%4*-1)}return{value:b,binLen:4*f+d}};break;case "TEXT":d=function(b,d,c){var f,n,m=0,h,l,p,t,q,r;d=d||[0];c=c||0;p=c>>>3;if("UTF8"===a)for(r=3,h=0;hf?n.push(f):2048>f?(n.push(192|
+ f>>>6),n.push(128|f&63)):55296>f||57344<=f?n.push(224|f>>>12,128|f>>>6&63,128|f&63):(h+=1,f=65536+((f&1023)<<10|b.charCodeAt(h)&1023),n.push(240|f>>>18,128|f>>>12&63,128|f>>>6&63,128|f&63)),l=0;l>>2;d.length<=t;)d.push(0);d[t]|=n[l]<<8*(r+q%4*-1);m+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(r=2,n="UTF16LE"===a&&!0||"UTF16LE"!==a&&!1,h=0;h>>8);q=m+p;for(t=q>>>2;d.length<=t;)d.push(0);d[t]|=f<<8*(r+q%4*-1);m+=
+ 2}return{value:d,binLen:8*m+c}};break;case "B64":d=function(a,b,d){var c=0,n,m,h,l,p,t,q;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");m=a.indexOf("=");a=a.replace(/\=/g,"");if(-1!==m&&m>>3;for(m=0;m>>2;b.length<=n;)b.push(0);b[n]|=(l>>>16-8*h&255)<<8*(3+q%4*-1);c+=1}}return{value:b,binLen:8*c+d}};break;case "BYTES":d=function(a,b,d){var c,n,m,h,l;b=b||[0];d=d||0;m=d>>>3;for(n=0;n>>2,b.length<=h&&b.push(0),b[h]|=c<<8*(3+l%4*-1);return{value:b,binLen:8*a.length+d}};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(e){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a,b,d){var c,n,m,h,l;b=b||[0];d=d||0;n=
+ d>>>3;l=new Uint8Array(a);for(c=0;c>>2,b.length<=m&&b.push(0),b[m]|=l[c]<<8*(3+h%4*-1);return{value:b,binLen:8*a.byteLength+d}};break;default:throw Error("format must be HEX, TEXT, B64, BYTES, or ARRAYBUFFER");}return d}function r(c,a){var d=null,d=new b(c.a,c.b);return d=32>=a?new b(d.a>>>a|d.b<<32-a&4294967295,d.b>>>a|d.a<<32-a&4294967295):new b(d.b>>>a-32|d.a<<64-a&4294967295,d.a>>>a-32|d.b<<64-a&4294967295)}function I(c,a){var d=null;return d=32>=a?new b(c.a>>>a,c.b>>>
+ a|c.a<<32-a&4294967295):new b(0,c.a>>>a-32)}function L(c,a,d){return new b(c.a&a.a^~c.a&d.a,c.b&a.b^~c.b&d.b)}function M(c,a,d){return new b(c.a&a.a^c.a&d.a^a.a&d.a,c.b&a.b^c.b&d.b^a.b&d.b)}function N(c){var a=r(c,28),d=r(c,34);c=r(c,39);return new b(a.a^d.a^c.a,a.b^d.b^c.b)}function O(c){var a=r(c,14),d=r(c,18);c=r(c,41);return new b(a.a^d.a^c.a,a.b^d.b^c.b)}function P(c){var a=r(c,1),d=r(c,8);c=I(c,7);return new b(a.a^d.a^c.a,a.b^d.b^c.b)}function Q(c){var a=r(c,19),d=r(c,61);c=I(c,6);return new b(a.a^
+ d.a^c.a,a.b^d.b^c.b)}function R(c,a){var d,e,g;d=(c.b&65535)+(a.b&65535);e=(c.b>>>16)+(a.b>>>16)+(d>>>16);g=(e&65535)<<16|d&65535;d=(c.a&65535)+(a.a&65535)+(e>>>16);e=(c.a>>>16)+(a.a>>>16)+(d>>>16);return new b((e&65535)<<16|d&65535,g)}function S(c,a,d,e){var g,k,f;g=(c.b&65535)+(a.b&65535)+(d.b&65535)+(e.b&65535);k=(c.b>>>16)+(a.b>>>16)+(d.b>>>16)+(e.b>>>16)+(g>>>16);f=(k&65535)<<16|g&65535;g=(c.a&65535)+(a.a&65535)+(d.a&65535)+(e.a&65535)+(k>>>16);k=(c.a>>>16)+(a.a>>>16)+(d.a>>>16)+(e.a>>>16)+(g>>>
+ 16);return new b((k&65535)<<16|g&65535,f)}function T(c,a,d,e,g){var k,f,r;k=(c.b&65535)+(a.b&65535)+(d.b&65535)+(e.b&65535)+(g.b&65535);f=(c.b>>>16)+(a.b>>>16)+(d.b>>>16)+(e.b>>>16)+(g.b>>>16)+(k>>>16);r=(f&65535)<<16|k&65535;k=(c.a&65535)+(a.a&65535)+(d.a&65535)+(e.a&65535)+(g.a&65535)+(f>>>16);f=(c.a>>>16)+(a.a>>>16)+(d.a>>>16)+(e.a>>>16)+(g.a>>>16)+(k>>>16);return new b((f&65535)<<16|k&65535,r)}function z(c){var a=[],d;if(0===c.lastIndexOf("SHA-",0))switch(a=[3238371032,914150663,812702999,4144912697,
+ 4290775857,1750603025,1694076839,3204075428],d=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],c){case "SHA-224":break;case "SHA-256":a=d;break;case "SHA-384":a=[new b(3418070365,a[0]),new b(1654270250,a[1]),new b(2438529370,a[2]),new b(355462360,a[3]),new b(1731405415,a[4]),new b(41048885895,a[5]),new b(3675008525,a[6]),new b(1203062813,a[7])];break;case "SHA-512":a=[new b(d[0],4089235720),new b(d[1],2227873595),new b(d[2],4271175723),new b(d[3],1595750129),
+ new b(d[4],2917565137),new b(d[5],725511199),new b(d[6],4215389547),new b(d[7],327033209)];break;default:throw Error("Unknown SHA variant");}else throw Error("No SHA variants supported");return a}function B(c,a,d){var e,g,k,f,r,m,h,l,p,t,q,y,u,x,v,A,z,B,C,D,E,F,w=[],G;if("SHA-384"===d||"SHA-512"===d)t=80,y=2,F=b,u=R,x=S,v=T,A=P,z=Q,B=N,C=O,E=M,D=L,G=J;else throw Error("Unexpected error in SHA-2 implementation");d=a[0];e=a[1];g=a[2];k=a[3];f=a[4];r=a[5];m=a[6];h=a[7];for(q=0;qq?(p=q*y,l=
+ c.length<=p?0:c[p],p=c.length<=p+1?0:c[p+1],w[q]=new F(l,p)):w[q]=x(z(w[q-2]),w[q-7],A(w[q-15]),w[q-16]),l=v(h,C(f),D(f,r,m),G[q],w[q]),p=u(B(d),E(d,e,g)),h=m,m=r,r=f,f=u(k,l),k=g,g=e,e=d,d=u(l,p);a[0]=u(d,a[0]);a[1]=u(e,a[1]);a[2]=u(g,a[2]);a[3]=u(k,a[3]);a[4]=u(f,a[4]);a[5]=u(r,a[5]);a[6]=u(m,a[6]);a[7]=u(h,a[7]);return a}var c,J;c=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,
+ 3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,
+ 3329325298];J=[new b(c[0],3609767458),new b(c[1],602891725),new b(c[2],3964484399),new b(c[3],2173295548),new b(c[4],4081628472),new b(c[5],3053834265),new b(c[6],2937671579),new b(c[7],3664609560),new b(c[8],2734883394),new b(c[9],1164996542),new b(c[10],1323610764),new b(c[11],3590304994),new b(c[12],4068182383),new b(c[13],991336113),new b(c[14],633803317),new b(c[15],3479774868),new b(c[16],2666613458),new b(c[17],944711139),new b(c[18],2341262773),new b(c[19],2007800933),new b(c[20],1495990901),
+ new b(c[21],1856431235),new b(c[22],3175218132),new b(c[23],2198950837),new b(c[24],3999719339),new b(c[25],766784016),new b(c[26],2566594879),new b(c[27],3203337956),new b(c[28],1034457026),new b(c[29],2466948901),new b(c[30],3758326383),new b(c[31],168717936),new b(c[32],1188179964),new b(c[33],1546045734),new b(c[34],1522805485),new b(c[35],2643833823),new b(c[36],2343527390),new b(c[37],1014477480),new b(c[38],1206759142),new b(c[39],344077627),new b(c[40],1290863460),new b(c[41],3158454273),
+ new b(c[42],3505952657),new b(c[43],106217008),new b(c[44],3606008344),new b(c[45],1432725776),new b(c[46],1467031594),new b(c[47],851169720),new b(c[48],3100823752),new b(c[49],1363258195),new b(c[50],3750685593),new b(c[51],3785050280),new b(c[52],3318307427),new b(c[53],3812723403),new b(c[54],2003034995),new b(c[55],3602036899),new b(c[56],1575990012),new b(c[57],1125592928),new b(c[58],2716904306),new b(c[59],442776044),new b(c[60],593698344),new b(c[61],3733110249),new b(c[62],2999351573),new b(c[63],
+ 3815920427),new b(3391569614,3928383900),new b(3515267271,566280711),new b(3940187606,3454069534),new b(4118630271,4000239992),new b(116418474,1914138554),new b(174292421,2731055270),new b(289380356,3203993006),new b(460393269,320620315),new b(685471733,587496836),new b(852142971,1086792851),new b(1017036298,365543100),new b(1126000580,2618297676),new b(1288033470,3409855158),new b(1501505948,4234509866),new b(1607167915,987167468),new b(1816402316,1246189591)];"function"===typeof define&&define.amd?
+ define(function(){return x}):"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(module.exports=x),exports=x):K.jsSHA=x})(this);
\ No newline at end of file
diff --git a/src/LoginCidadao/SupportBundle/Resources/translations/messages.en.yml b/src/LoginCidadao/SupportBundle/Resources/translations/messages.en.yml
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/LoginCidadao/SupportBundle/Resources/translations/messages.pt_BR.yml b/src/LoginCidadao/SupportBundle/Resources/translations/messages.pt_BR.yml
new file mode 100644
index 000000000..62c197133
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/translations/messages.pt_BR.yml
@@ -0,0 +1,62 @@
+support:
+ search:
+ menu:
+ support: "Painel de Suporte"
+ search: "Pesquisar Usuários"
+ title: "Pesquisar Usuário"
+ submit: "Pesquisar"
+ grid:
+ first_name: "Nome"
+ last_name: "Sobrenome"
+ cpf: "CPF"
+ phone_number: "Telefone"
+ email: "Email"
+ details: "Ver Detalhes"
+ load_more: "Carregar mais"
+ person:
+ title: "Informações de %first_name%"
+ view:
+ date_format: "d/m/Y"
+ datetime_format: "d/m/Y H:i:s"
+ first_name.label: "Nome"
+ last_name.label: "Sobrenome"
+ birthday.label: "Data de Nascimento"
+ cpf.label: "CPF"
+ email.label: "Email"
+ emailVerifiedAt.label: "Data de verificação do email"
+ phoneNumber:
+ label: "Telefone"
+ usersWithSamePhone: "{0} Nenhuma conta utiliza esse telefone|{1} Apenas essa conta utiliza esse telefone|]1,Inf[ %count% pessoas estão usando esse mesmo número!"
+ phoneNumberVerification:
+ label: "Data de Verificação do Telefone"
+ not_verified: "NÃO verificado"
+ lastPasswordResetRequest.label: "Última solicitação de recuperação de senha"
+ 2FA:
+ label: "Autenticação de 2 Fatores ativa?"
+ enabled: "Autenticação de 2 Fatores ATIVA"
+ disabled: "SEM Autenticação de 2 Fatores"
+ enabled:
+ label: "Usuário ativo?"
+ enabled: "Usuário ativo"
+ disabled: "Usuário BLOQUEADO!"
+ lastUpdate.label: "Data da Última Atualização"
+ createdAt.label: "Data de Criação da Conta"
+ lastLogin.label: "Último Login"
+ connections:
+ enabled: "Conectado"
+ disabled: "Desconectado"
+ facebook.label: "Facebook"
+ twitter.label: "Twitter"
+ google.label: "Google"
+ validation:
+ title: "Validação de Dados Pessoais"
+ cpf: "Verificar CPF"
+ birthday: "Verificar data de nascimento"
+ email: "Verificar email"
+ phoneNumber: "Verificar telefone"
+ initialContact:
+ title: "Mensagem Inicial"
+ about: "Você está acessando esse perfil para atender a demanda originada na seguinte mensagem:"
+ subject: "Título da Mensagem Inicial"
+ date: "Data da Mensagem Inicial"
+ message: "Mensagem Inicial"
diff --git a/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/grid.html.twig b/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/grid.html.twig
new file mode 100644
index 000000000..c32734c26
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/grid.html.twig
@@ -0,0 +1,67 @@
+{% extends "LoginCidadaoCoreBundle::grid_layout.html.twig" %}
+
+{% block grid_header_data %}
+ {{ 'support.search.grid.first_name' | trans }}
+ {{ 'support.search.grid.last_name' | trans }}
+ {{ 'support.search.grid.cpf' | trans }}
+ {{ 'support.search.grid.phone_number' | trans }}
+ {{ 'support.search.grid.email' | trans }}
+{% endblock grid_header_data %}
+
+{% block grid_row_action %}
+
+{% endblock grid_row_action %}
+
+{% block grid_row_data %}
+ {% if row.phoneNumber.countryCode == 55 %}
+ {% set format = 'NATIONAL' %}
+ {% else %}
+ {% set format = 'INTERNATIONAL' %}
+ {% endif %}
+
+
+
{{ 'support.search.grid.first_name' | trans }}
+ {{ row.firstName }}
+
+
+
{{ 'support.search.grid.last_name' | trans }}
+ {{ row.surname }}
+
+
+
{{ 'support.search.grid.cpf' | trans }}
+ {{ row.cpf | formatCpf | support_mask(not is_granted('ROLE_VIEW_USERS_CPF')) }}
+
+
+
{{ 'support.search.grid.phone_number' | trans }}
+ {{ row.phoneNumber | phone_number_format(format) | support_mask(not is_granted('ROLE_SUPPORT_VIEW_PHONE')) }}
+
+
+
{{ 'support.search.grid.email' | trans }}
+ {{ row.email | support_mask(not is_granted('ROLE_SUPPORT_VIEW_EMAIL')) }}
+
+{% endblock grid_row_data %}
+
+{% block grid_infinite_pagination %}
+
+ {% if not grid.getRlast and grid.isInfiniteGrid %}
+ {% set routeParams = { 'page': grid.page + 1 } %}
+
+
+
+ {% endif %}
+
+{% endblock grid_infinite_pagination %}
diff --git a/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/index.html.twig b/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/index.html.twig
new file mode 100644
index 000000000..771c28cc4
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/index.html.twig
@@ -0,0 +1,32 @@
+{% extends "LoginCidadaoSupportBundle::base.html.twig" %}
+{% block content %}
+
+
+
+
{{ 'support.search.title' | trans }}
+
+
+
+
+ {{ form_start(form) }}
+ {{ form_errors(form) }}
+
+
+ {{ form_row(form.supportTicket) }}
+ {{ form_row(form.smartSearch) }}
+
+
+
+ {{ form_end(form) }}
+
+
+
+
+
+
+ {% if grid %}
+ {{ include("LoginCidadaoSupportBundle:PersonSupport:grid.html.twig") }}
+ {% endif %}
+{% endblock %}
diff --git a/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/view.html.twig b/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/view.html.twig
new file mode 100644
index 000000000..d538100ae
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/views/PersonSupport/view.html.twig
@@ -0,0 +1,146 @@
+{% extends "LoginCidadaoSupportBundle::base.html.twig" %}
+{% block content %}
+
+
+
+
{{ 'support.person.title' | trans({'%first_name%': person.name}) }}
+
+
+
+
+
+ - {{ 'support.person.view.first_name.label' | trans }}
+ - {{ person.firstName }}
+ - {{ 'support.person.view.last_name.label' | trans }}
+ - {{ person.lastName }}
+ - {{ 'support.person.view.birthday.label' | trans }}
+ - {{ person.birthday }}
+ - {{ 'support.person.view.cpf.label' | trans }}
+ - {{ person.cpf | formatCpf }}
+ - {{ 'support.person.view.email.label' | trans }}
+ - {{ person.email }}
+ - {{ 'support.person.view.emailVerifiedAt.label' | trans }}
+ - {{ person.emailVerifiedAt is empty ? "-" : person.emailVerifiedAt | date('support.person.view.datetime_format' | trans) }}
+ - {{ 'support.person.view.phoneNumber.label' | trans }}
+ - {{ person.phoneNumber }}
+ - {{ 'support.person.view.phoneNumber.usersWithSamePhone' | transchoice(phoneMetadata.samePhoneCount) }}
+ - {{ 'support.person.view.phoneNumberVerification.label' | trans }}
+ - {{ phoneMetadata.verification is empty ? 'support.person.view.phoneNumberVerification.not_verified' | trans : phoneMetadata.verification.verifiedAt | date('support.person.view.datetime_format' | trans) }}
+ - {{ 'support.person.view.lastPasswordResetRequest.label' | trans }}
+ - {{ person.lastPasswordResetRequest is empty ? "-" : person.lastPasswordResetRequest | date('support.person.view.datetime_format' | trans) }}
+ - {{ 'support.person.view.2FA.label' | trans }}
+ - {{ (person.has2FA ? 'support.person.view.2FA.enabled' : 'support.person.view.2FA.disabled') | trans }}
+ - {{ 'support.person.view.enabled.label' | trans }}
+ - {{ (person.enabled ? 'support.person.view.enabled.enabled' : 'support.person.view.enabled.disabled') | trans }}
+ - {{ 'support.person.view.lastUpdate.label' | trans }}
+ - {{ person.lastUpdate is empty ? "-" : person.lastUpdate | date('support.person.view.datetime_format' | trans) }}
+ - {{ 'support.person.view.createdAt.label' | trans }}
+ - {{ person.createdAt | date('support.person.view.datetime_format' | trans) }}
+ - {{ 'support.person.view.lastLogin.label' | trans }}
+ - {{ person.lastLogin | date('support.person.view.datetime_format' | trans) }}
+
+
+
+ {% for thirdParty, connected in thirdPartyConnections %}
+ - {{ ('support.person.view.connections.' ~ thirdParty ~ '.label') | trans }}
+ - {{ (connected ? 'support.person.view.connections.enabled' : 'support.person.view.connections.disabled') | trans }}
+ {% endfor %}
+
+
+
+
+
+
+ {% if dataValidation | length > 0 %}
+
+
+
{{ 'support.person.validation.title' | trans }}
+
+
+
+ {% endif %}
+
+ {% if supportRequest is not null %}
+
+
+
{{ 'support.person.initialContact.title' | trans }}
+
+
+
+
+
{{ 'support.person.initialContact.about' | trans }}
+
+ - {{ 'support.person.initialContact.subject' | trans }}
+ - {{ supportRequest.subject }}
+ - {{ 'support.person.initialContact.date' | trans }}
+ - {{ supportRequest.date | date }}
+ - {{ 'support.person.initialContact.message' | trans }}
+ - {{ supportRequest.message }}
+
+
+
+
+
+ {% endif %}
+
+{% endblock %}
+
+{% block javascripts %}
+ {{ parent() }}
+
+ {% javascripts
+ '@LoginCidadaoSupportBundle/Resources/public/js/sha512.js'
+ filter='uglifyjs2' %}
+
+ {% endjavascripts %}
+
+{% endblock javascripts %}
diff --git a/src/LoginCidadao/SupportBundle/Resources/views/base.html.twig b/src/LoginCidadao/SupportBundle/Resources/views/base.html.twig
new file mode 100644
index 000000000..13402d2d4
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/views/base.html.twig
@@ -0,0 +1,11 @@
+{% extends "LoginCidadaoCoreBundle::base.loggedIn.html.twig" %}
+
+{% block sidebar %}
+ {{ include("LoginCidadaoSupportBundle::extensible.sidebar.html.twig") }}
+{% endblock %}
+
+{% block content %}
+
+ EMPTY
+
+{% endblock content %}
diff --git a/src/LoginCidadao/SupportBundle/Resources/views/extensible.sidebar.html.twig b/src/LoginCidadao/SupportBundle/Resources/views/extensible.sidebar.html.twig
new file mode 100644
index 000000000..bca1a50a4
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/views/extensible.sidebar.html.twig
@@ -0,0 +1 @@
+{% extends "LoginCidadaoSupportBundle::sidebar.html.twig" %}
diff --git a/src/LoginCidadao/SupportBundle/Resources/views/sidebar.html.twig b/src/LoginCidadao/SupportBundle/Resources/views/sidebar.html.twig
new file mode 100644
index 000000000..df16b96ec
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Resources/views/sidebar.html.twig
@@ -0,0 +1,21 @@
+{% set myController = app.request.attributes.get("_controller") | trim %}
+
diff --git a/src/LoginCidadao/SupportBundle/Service/SupportHandler.php b/src/LoginCidadao/SupportBundle/Service/SupportHandler.php
new file mode 100644
index 000000000..274bf0516
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Service/SupportHandler.php
@@ -0,0 +1,137 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Service;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Entity\SentEmail;
+use LoginCidadao\CoreBundle\Entity\SentEmailRepository;
+use LoginCidadao\CoreBundle\Model\IdentifiablePersonInterface;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationServiceInterface;
+use LoginCidadao\SupportBundle\Exception\PersonNotFoundException;
+use LoginCidadao\SupportBundle\Model\PersonalData;
+use LoginCidadao\SupportBundle\Model\SupportPerson;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+
+class SupportHandler implements SupportHandlerInterface
+{
+ /** @var AuthorizationCheckerInterface */
+ private $authChecker;
+
+ /** @var PhoneVerificationServiceInterface */
+ private $phoneVerificationService;
+
+ /** @var PersonRepository */
+ private $personRepository;
+
+ /** @var SentEmailRepository */
+ private $sentEmailRepository;
+
+ /**
+ * SupportHandler constructor.
+ * @param AuthorizationCheckerInterface $authChecker
+ * @param PhoneVerificationServiceInterface $phoneVerificationService
+ * @param PersonRepository $personRepository
+ * @param SentEmailRepository $sentEmailRepository
+ */
+ public function __construct(
+ AuthorizationCheckerInterface $authChecker,
+ PhoneVerificationServiceInterface $phoneVerificationService,
+ PersonRepository $personRepository,
+ SentEmailRepository $sentEmailRepository
+ ) {
+ $this->authChecker = $authChecker;
+ $this->phoneVerificationService = $phoneVerificationService;
+ $this->personRepository = $personRepository;
+ $this->sentEmailRepository = $sentEmailRepository;
+ }
+
+ public function getSupportPerson($id): SupportPerson
+ {
+ $person = $this->personRepository->find($id);
+ if (!$person instanceof PersonInterface) {
+ throw new PersonNotFoundException();
+ }
+
+ return new SupportPerson($person, $this->authChecker);
+ }
+
+ public function getThirdPartyConnections(IdentifiablePersonInterface $person): array
+ {
+ $connections = [];
+ if (!$person instanceof PersonInterface) {
+ /** @var PersonInterface $person */
+ $person = $this->personRepository->find($person->getId());
+ }
+
+ if ($person instanceof PersonInterface) {
+ $connections = [
+ 'facebook' => $person->getFacebookId() !== null,
+ 'google' => $person->getGoogleId() !== null,
+ 'twitter' => $person->getTwitterId() !== null,
+ ];
+ }
+
+ return $connections;
+ }
+
+ public function getPhoneMetadata(IdentifiablePersonInterface $person): array
+ {
+ /** @var PersonInterface $person */
+ $person = $this->personRepository->find($person->getId());
+ $phone = $person->getMobile();
+
+ if ($phone instanceof PhoneNumber) {
+ $samePhoneCount = $this->personRepository->countByPhone($phone);
+ $phoneVerification = $this->phoneVerificationService->getPhoneVerification($person, $phone);
+ }
+
+ return [
+ 'samePhoneCount' => $samePhoneCount ?? 0,
+ 'verification' => $phoneVerification ?? null,
+ ];
+ }
+
+ public function getInitialMessage($ticket): ?SentEmail
+ {
+ /** @var SentEmail $sentEmail */
+ $sentEmail = $this->sentEmailRepository->findOneBy(['supportTicket' => $ticket]);
+
+ return $sentEmail;
+ }
+
+ public function getValidationMap(SupportPerson $person): array
+ {
+ return array_filter([
+ 'cpf' => $this->personalDataToValidationArray($person->getCpf(), true),
+ 'birthday' => $this->personalDataToValidationArray($person->getBirthday(), true),
+ 'email' => $this->personalDataToValidationArray($person->getEmail(), true),
+ 'phoneNumber' => $this->personalDataToValidationArray($person->getPhoneNumber(), true),
+ ]);
+ }
+
+ private function personalDataToValidationArray(PersonalData $data, bool $skipIfValueSet = false): ?array
+ {
+ if (false === $data->isValueFilled()) {
+ return null;
+ }
+ if ($skipIfValueSet && $data->getValue() !== null) {
+ return null;
+ }
+
+ return [
+ 'name' => $data->getName(),
+ 'hash' => $data->getHash(),
+ 'challenge' => $data->getChallenge(),
+ ];
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Service/SupportHandlerInterface.php b/src/LoginCidadao/SupportBundle/Service/SupportHandlerInterface.php
new file mode 100644
index 000000000..9ebfb6199
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Service/SupportHandlerInterface.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Service;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Entity\SentEmail;
+use LoginCidadao\CoreBundle\Entity\SentEmailRepository;
+use LoginCidadao\CoreBundle\Model\IdentifiablePersonInterface;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationServiceInterface;
+use LoginCidadao\SupportBundle\Exception\PersonNotFoundException;
+use LoginCidadao\SupportBundle\Model\PersonalData;
+use LoginCidadao\SupportBundle\Model\SupportPerson;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+
+interface SupportHandlerInterface
+{
+ public function getSupportPerson($id): SupportPerson;
+
+ public function getThirdPartyConnections(IdentifiablePersonInterface $person): array;
+
+ public function getPhoneMetadata(IdentifiablePersonInterface $person): array;
+
+ public function getInitialMessage($ticket): ?SentEmail;
+
+ public function getValidationMap(SupportPerson $person): array;
+}
diff --git a/src/LoginCidadao/SupportBundle/Tests/Form/PersonSearchFormTypeTest.php b/src/LoginCidadao/SupportBundle/Tests/Form/PersonSearchFormTypeTest.php
new file mode 100644
index 000000000..61d8cf6dc
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Tests/Form/PersonSearchFormTypeTest.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Tests\Form;
+
+use LoginCidadao\SupportBundle\Form\PersonSearchFormType;
+use LoginCidadao\SupportBundle\Model\PersonSearchRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class PersonSearchFormTypeTest extends TestCase
+{
+ public function testForm()
+ {
+ /** @var OptionsResolver|MockObject $resolver */
+ $resolver = $this->createMock(OptionsResolver::class);
+ $resolver->expects($this->once())->method('setDefaults')->with([
+ 'data_class' => PersonSearchRequest::class,
+ 'csrf_protection' => false,
+ ]);
+
+ $builder = $this->createMock(FormBuilderInterface::class);
+ $builder->expects($this->once())->method('setMethod')->with('GET')->willReturn($builder);
+ $builder->expects($this->exactly(2))->method('add')
+ ->willReturnCallback(function ($name, $type, $options) use ($builder) {
+ $this->assertContains($name, ['supportTicket', 'smartSearch']);
+ $this->assertEquals(TextType::class, $type);
+ if (!empty($options)) {
+ $this->assertArrayHasKey('label', $options);
+ }
+
+ return $builder;
+ });
+
+ $form = new PersonSearchFormType();
+ $form->configureOptions($resolver);
+ $form->buildForm($builder, []);
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Tests/Model/PersonalDataTest.php b/src/LoginCidadao/SupportBundle/Tests/Model/PersonalDataTest.php
new file mode 100644
index 000000000..f61e39c52
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Tests/Model/PersonalDataTest.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Tests\Model;
+
+use LoginCidadao\SupportBundle\Model\PersonalData;
+use PHPUnit\Framework\TestCase;
+
+class PersonalDataTest extends TestCase
+{
+ public function testKnownValue()
+ {
+ $data = PersonalData::createWithValue($name = 'privateInfo', $value = 'my secret value');
+
+ $this->assertSame($name, $data->getName());
+ $this->assertSame($value, $data->getValue());
+ $this->assertNotNull($data->getHash());
+ $this->assertNotNull($data->getChallenge());
+ $this->assertTrue($data->checkValue($value));
+ $this->assertTrue($data->isValueFilled());
+ $this->assertEquals($value, $data->__toString());
+ }
+
+ public function testKnownEmptyValue()
+ {
+ $data = PersonalData::createWithValue($name = 'privateInfo', $value = null);
+
+ $this->assertSame($name, $data->getName());
+ $this->assertSame('', $data->getValue());
+ $this->assertNotNull($data->getHash());
+ $this->assertNotNull($data->getChallenge());
+ $this->assertTrue($data->checkValue(''));
+ $this->assertFalse($data->isValueFilled());
+ $this->assertEquals('', $data->__toString());
+ }
+
+ public function testUnknownValue()
+ {
+ $value = 'my unknown secret value';
+ $data = PersonalData::createWithoutValue($name = 'privateInfo', $value);
+
+ $this->assertSame($name, $data->getName());
+ $this->assertNull($data->getValue());
+ $this->assertNotNull($data->getHash());
+ $this->assertNotNull($data->getChallenge());
+ $this->assertTrue($data->checkValue($value));
+ $this->assertTrue($data->isValueFilled());
+ $this->assertEquals('Yes', $data->__toString());
+ }
+
+ public function testCreateInvalidObject()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ new PersonalData('invalid', null, true, null);
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Tests/Model/SupportPersonTest.php b/src/LoginCidadao/SupportBundle/Tests/Model/SupportPersonTest.php
new file mode 100644
index 000000000..61484249e
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Tests/Model/SupportPersonTest.php
@@ -0,0 +1,146 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Tests\Model;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\Person;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\SupportBundle\Model\PersonalData;
+use LoginCidadao\SupportBundle\Model\SupportPerson;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+
+class SupportPersonTest extends TestCase
+{
+ public function testMinimumAccess()
+ {
+ $authChecker = $this->getAuthChecker([]);
+
+ $phone = (new PhoneNumber())->setNationalNumber(55)->setNationalNumber(51999998888);
+
+ /** @var MockObject|PersonInterface $person */
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getId')->willReturn($id = 123);
+ $person->expects($this->once())->method('getFirstName')->willReturn($firstName = 'John');
+ $person->expects($this->once())->method('getSurname')->willReturn($lastName = 'Doe');
+ $person->expects($this->once())->method('getBirthdate')->willReturn(new \DateTime());
+ $person->expects($this->once())->method('getEmailConfirmedAt')->willReturn($emailVerified = new \DateTime());
+ $person->expects($this->once())->method('getPasswordRequestedAt')->willReturn($lastPwReset = null);
+ $person->expects($this->once())->method('getGoogleAuthenticatorSecret')->willReturn('1234');
+ $person->expects($this->once())->method('isEnabled')->willReturn(true);
+ $person->expects($this->once())->method('getUpdatedAt')->willReturn($lastUpdate = new \DateTime());
+ $person->expects($this->once())->method('getCreatedAt')->willReturn($createdAt = new \DateTime());
+ $person->expects($this->once())->method('getLastLogin')->willReturn($lastLogin = new \DateTime());
+ $person->expects($this->once())->method('getMobile')->willReturn($phone);
+
+ $supportPerson = new SupportPerson($person, $authChecker);
+ $this->assertInstanceOf(PersonalData::class, $supportPerson->getBirthday());
+ $this->assertNull($supportPerson->getBirthday()->getValue());
+
+ $this->assertInstanceOf(PersonalData::class, $supportPerson->getCpf());
+ $this->assertNull($supportPerson->getCpf()->getValue());
+
+ $this->assertInstanceOf(PersonalData::class, $supportPerson->getEmail());
+ $this->assertNull($supportPerson->getEmail()->getValue());
+
+ $this->assertInstanceOf(PersonalData::class, $supportPerson->getPhoneNumber());
+ $this->assertNull($supportPerson->getPhoneNumber()->getValue());
+
+ $this->assertSame($id, $supportPerson->getId());
+ $this->assertSame($firstName, $supportPerson->getFirstName());
+ $this->assertSame($lastName, $supportPerson->getLastName());
+ $this->assertSame($emailVerified, $supportPerson->getEmailVerifiedAt());
+ $this->assertSame($lastPwReset, $supportPerson->getLastPasswordResetRequest());
+ $this->assertSame($lastUpdate, $supportPerson->getLastUpdate());
+ $this->assertSame($createdAt, $supportPerson->getCreatedAt());
+ $this->assertSame($lastLogin, $supportPerson->getLastLogin());
+ $this->assertTrue($supportPerson->has2FA());
+ $this->assertTrue($supportPerson->isEnabled());
+ }
+
+ public function testFullAccess()
+ {
+ $authChecker = $this->getAuthChecker([
+ 'ROLE_VIEW_USERS_CPF',
+ 'ROLE_SUPPORT_VIEW_EMAIL',
+ 'ROLE_SUPPORT_VIEW_PHONE',
+ 'ROLE_SUPPORT_VIEW_BIRTHDAY'
+ ]);
+
+ $phone = (new PhoneNumber())->setNationalNumber(55)->setNationalNumber(51999998888);
+
+ /** @var MockObject|PersonInterface $person */
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getId')->willReturn($id = 123);
+ $person->expects($this->once())->method('getFirstName')->willReturn($firstName = 'John');
+ $person->expects($this->once())->method('getSurname')->willReturn($lastName = 'Doe');
+ $person->expects($this->once())->method('getBirthdate')->willReturn(new \DateTime());
+ $person->expects($this->once())->method('getEmailConfirmedAt')->willReturn($emailVerified = new \DateTime());
+ $person->expects($this->once())->method('getPasswordRequestedAt')->willReturn($lastPwReset = null);
+ $person->expects($this->once())->method('getGoogleAuthenticatorSecret')->willReturn('1234');
+ $person->expects($this->once())->method('isEnabled')->willReturn(true);
+ $person->expects($this->once())->method('getUpdatedAt')->willReturn($lastUpdate = new \DateTime());
+ $person->expects($this->once())->method('getCreatedAt')->willReturn($createdAt = new \DateTime());
+ $person->expects($this->once())->method('getMobile')->willReturn($phone);
+
+ $supportPerson = new SupportPerson($person, $authChecker);
+ $this->assertInstanceOf(PersonalData::class, $supportPerson->getBirthday());
+ $this->assertNotNull($supportPerson->getBirthday()->getValue());
+
+ $this->assertInstanceOf(PersonalData::class, $supportPerson->getCpf());
+ $this->assertNotNull($supportPerson->getCpf()->getValue());
+
+ $this->assertInstanceOf(PersonalData::class, $supportPerson->getEmail());
+ $this->assertNotNull($supportPerson->getEmail()->getValue());
+
+ $this->assertInstanceOf(PersonalData::class, $supportPerson->getPhoneNumber());
+ $this->assertNotNull($supportPerson->getPhoneNumber()->getValue());
+
+ $this->assertSame($id, $supportPerson->getId());
+ $this->assertSame($firstName, $supportPerson->getFirstName());
+ $this->assertSame($lastName, $supportPerson->getLastName());
+ $this->assertSame($emailVerified, $supportPerson->getEmailVerifiedAt());
+ $this->assertSame($lastPwReset, $supportPerson->getLastPasswordResetRequest());
+ $this->assertSame($lastUpdate, $supportPerson->getLastUpdate());
+ $this->assertSame($createdAt, $supportPerson->getCreatedAt());
+ $this->assertTrue($supportPerson->has2FA());
+ $this->assertTrue($supportPerson->isEnabled());
+ }
+
+ public function testGetName()
+ {
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getId')->willReturn($id = 123);
+ $supportPerson = new SupportPerson($person, $this->getAuthChecker([]));
+
+ $this->assertEquals($id, $supportPerson->getName());
+
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getId')->willReturn(123);
+ $person->expects($this->once())->method('getFirstName')->willReturn($firstName = 'John');
+ $supportPerson = new SupportPerson($person, $this->getAuthChecker([]));
+
+ $this->assertEquals($firstName, $supportPerson->getName());
+ }
+
+ private function getAuthChecker(array $roles = [])
+ {
+ /** @var MockObject|AuthorizationCheckerInterface $authChecker */
+ $authChecker = $this->createMock(AuthorizationCheckerInterface::class);
+ $authChecker->expects($this->exactly(4))->method('isGranted')
+ ->willReturnCallback(function ($role) use ($roles) {
+ return false !== array_search($role, $roles);
+ });
+
+ return $authChecker;
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Tests/Service/SupportHandlerTest.php b/src/LoginCidadao/SupportBundle/Tests/Service/SupportHandlerTest.php
new file mode 100644
index 000000000..57b5b51ff
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Tests/Service/SupportHandlerTest.php
@@ -0,0 +1,325 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Tests\Service;
+
+use libphonenumber\PhoneNumber;
+use LoginCidadao\CoreBundle\Entity\Person;
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Entity\SentEmail;
+use LoginCidadao\CoreBundle\Entity\SentEmailRepository;
+use LoginCidadao\CoreBundle\Model\IdentifiablePersonInterface;
+use LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface;
+use LoginCidadao\PhoneVerificationBundle\Service\PhoneVerificationServiceInterface;
+use LoginCidadao\SupportBundle\Exception\PersonNotFoundException;
+use LoginCidadao\SupportBundle\Model\PersonalData;
+use LoginCidadao\SupportBundle\Model\SupportPerson;
+use LoginCidadao\SupportBundle\Service\SupportHandler;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+
+class SupportHandlerTest extends TestCase
+{
+ public function testGetSupportPerson()
+ {
+ $id = 123;
+ $person = $this->createMock(Person::class);
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $this->getPhoneVerificationService(),
+ $this->getPersonRepo($id, $person),
+ $this->getSentEmailRepo()
+ );
+ $supportPerson = $handler->getSupportPerson($id);
+
+ $this->assertInstanceOf(SupportPerson::class, $supportPerson);
+ }
+
+ public function testGetNonExistingSupportPerson()
+ {
+ $this->expectException(PersonNotFoundException::class);
+ $id = 123;
+
+ $handler = new SupportHandler($this->getAuthChecker(), $this->getPhoneVerificationService(),
+ $this->getPersonRepo($id, null),
+ $this->getSentEmailRepo());
+ $handler->getSupportPerson($id);
+ }
+
+ public function testGetInitialMessage()
+ {
+ $id = 123;
+ $sentEmail = $this->createMock(SentEmail::class);
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $this->getPhoneVerificationService(),
+ $this->getPersonRepo(),
+ $this->getSentEmailRepo($id, $sentEmail)
+ );
+
+ $this->assertSame($sentEmail, $handler->getInitialMessage($id));
+ }
+
+ public function testGetValidationMap()
+ {
+ $person = $this->createMock(SupportPerson::class);
+ $person->expects($this->once())->method('getCpf')
+ ->willReturn($this->getPersonalData(null, null, null, 'cpf', true));
+
+ $person->expects($this->once())->method('getBirthday')
+ ->willReturn($this->getPersonalData(null, null, null, null, false));
+
+ $person->expects($this->once())->method('getEmail')
+ ->willReturn($this->getPersonalData('email', 'hash', 'challenge', null, true));
+
+ $person->expects($this->once())->method('getPhoneNumber')
+ ->willReturn($this->getPersonalData('phoneNumber', 'hash', 'challenge', null, true));
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $this->getPhoneVerificationService(),
+ $this->getPersonRepo(),
+ $this->getSentEmailRepo()
+ );
+
+ $map = $handler->getValidationMap($person);
+
+ $this->assertNotEmpty($map);
+ $this->assertArrayNotHasKey('birthday', $map);
+ $this->assertArrayNotHasKey('cpf', $map);
+ // Phone
+ $this->assertEquals('challenge', $map['phoneNumber']['challenge']);
+ $this->assertEquals('hash', $map['phoneNumber']['hash']);
+ // Email
+ $this->assertEquals('challenge', $map['email']['challenge']);
+ $this->assertEquals('hash', $map['email']['hash']);
+ }
+
+ public function testGetPhoneMetadata()
+ {
+ $id = 321;
+ $phoneNumber = new PhoneNumber();
+
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getMobile')->willReturn($phoneNumber);
+
+ $supportPerson = $this->createMock(SupportPerson::class);
+ $supportPerson->expects($this->once())->method('getId')->willReturn($id);
+
+ $personRepo = $this->getPersonRepo($id, $person);
+ $personRepo->expects($this->once())->method('countByPhone')->with($phoneNumber)->willReturn(3);
+
+ $verification = $this->createMock(PhoneVerificationInterface::class);
+ $verificationService = $this->getPhoneVerificationService();
+ $verificationService->expects($this->once())
+ ->method('getPhoneVerification')->with($person, $phoneNumber)->willReturn($verification);
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $verificationService,
+ $personRepo,
+ $this->getSentEmailRepo()
+ );
+ $metadata = $handler->getPhoneMetadata($supportPerson);
+
+ $this->assertEquals(3, $metadata['samePhoneCount']);
+ $this->assertSame($verification, $metadata['verification']);
+ }
+
+ public function testGetPhoneMetadataNoVerification()
+ {
+ $id = 321;
+ $phoneNumber = new PhoneNumber();
+
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getMobile')->willReturn($phoneNumber);
+
+ $supportPerson = $this->createMock(SupportPerson::class);
+ $supportPerson->expects($this->once())->method('getId')->willReturn($id);
+
+ $personRepo = $this->getPersonRepo($id, $person);
+ $personRepo->expects($this->once())->method('countByPhone')->with($phoneNumber)->willReturn(3);
+
+ $verificationService = $this->getPhoneVerificationService();
+ $verificationService->expects($this->once())
+ ->method('getPhoneVerification')->with($person, $phoneNumber)->willReturn(null);
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $verificationService,
+ $personRepo,
+ $this->getSentEmailRepo()
+ );
+ $metadata = $handler->getPhoneMetadata($supportPerson);
+
+ $this->assertEquals(3, $metadata['samePhoneCount']);
+ $this->assertNull($metadata['verification']);
+ }
+
+ public function testGetPhoneMetadataNoPhone()
+ {
+ $id = 321;
+
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getMobile')->willReturn(null);
+
+ $supportPerson = $this->createMock(SupportPerson::class);
+ $supportPerson->expects($this->once())->method('getId')->willReturn($id);
+
+ $personRepo = $this->getPersonRepo($id, $person);
+ $personRepo->expects($this->never())->method('countByPhone');
+
+ $verificationService = $this->getPhoneVerificationService();
+ $verificationService->expects($this->never())->method('getPhoneVerification');
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $verificationService,
+ $personRepo,
+ $this->getSentEmailRepo()
+ );
+ $metadata = $handler->getPhoneMetadata($supportPerson);
+
+ $this->assertEquals(0, $metadata['samePhoneCount']);
+ $this->assertNull($metadata['verification']);
+ }
+
+ public function testThirdPartyConnections()
+ {
+ /** @var IdentifiablePersonInterface|MockObject $identifiablePerson */
+ $identifiablePerson = $this->createMock(IdentifiablePersonInterface::class);
+ $identifiablePerson->expects($this->once())->method('getId')->willReturn($id = 666);
+
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getFacebookId')->willReturn('facebook');
+ $person->expects($this->once())->method('getGoogleId')->willReturn('google');
+ $person->expects($this->once())->method('getTwitterId')->willReturn('twitter');
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $this->getPhoneVerificationService(),
+ $this->getPersonRepo($id, $person),
+ $this->getSentEmailRepo()
+ );
+ $this->assertEquals([
+ 'google' => true,
+ 'facebook' => true,
+ 'twitter' => true,
+ ], $handler->getThirdPartyConnections($identifiablePerson));
+ }
+
+ public function testThirdPartyConnectionsWithPersonInterface()
+ {
+ $person = $this->createMock(Person::class);
+ $person->expects($this->once())->method('getFacebookId')->willReturn('facebook');
+ $person->expects($this->once())->method('getGoogleId')->willReturn('google');
+ $person->expects($this->once())->method('getTwitterId')->willReturn('twitter');
+
+ $personRepo = $this->getPersonRepo();
+ $personRepo->expects($this->never())->method('find');
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $this->getPhoneVerificationService(),
+ $personRepo,
+ $this->getSentEmailRepo()
+ );
+ $this->assertEquals([
+ 'google' => true,
+ 'facebook' => true,
+ 'twitter' => true,
+ ], $handler->getThirdPartyConnections($person));
+ }
+
+ public function testThirdPartyConnectionsUserNotFound()
+ {
+ /** @var IdentifiablePersonInterface|MockObject $identifiablePerson */
+ $identifiablePerson = $this->createMock(IdentifiablePersonInterface::class);
+ $identifiablePerson->expects($this->once())->method('getId')->willReturn($id = 666);
+
+ $handler = new SupportHandler(
+ $this->getAuthChecker(),
+ $this->getPhoneVerificationService(),
+ $this->getPersonRepo($id, null),
+ $this->getSentEmailRepo()
+ );
+ $this->assertEmpty($handler->getThirdPartyConnections($identifiablePerson));
+ }
+
+ private function getPersonalData(?string $name, ?string $hash, ?string $challenge, $value, bool $filled)
+ {
+ $data = $this->createMock(PersonalData::class);
+ $data->expects($this->once())->method('isValueFilled')->willReturn($filled);
+ if (null !== $value) {
+ $data->expects($this->once())->method('getValue')->willReturn($value);
+ }
+ if (null !== $challenge) {
+ $data->expects($this->once())->method('getChallenge')->willReturn($challenge);
+ }
+ if (null !== $name) {
+ $data->expects($this->once())->method('getName')->willReturn($name);
+ }
+ if (null !== $hash) {
+ $data->expects($this->once())->method('getHash')->willReturn($hash);
+ }
+
+ return $data;
+ }
+
+ /**
+ * @return MockObject|AuthorizationCheckerInterface
+ */
+ private function getAuthChecker()
+ {
+ return $this->createMock(AuthorizationCheckerInterface::class);
+ }
+
+ /**
+ * @param null $id
+ * @param null $person
+ * @return MockObject|PersonRepository
+ */
+ private function getPersonRepo($id = null, $person = null)
+ {
+ $personRepo = $this->createMock(PersonRepository::class);
+ if (null !== $id) {
+ $personRepo->expects($this->once())->method('find')->with($id)->willReturn($person);
+ }
+
+ return $personRepo;
+ }
+
+ /**
+ * @param null $id
+ * @param null $sentEmail
+ * @return MockObject|SentEmailRepository
+ */
+ private function getSentEmailRepo($id = null, $sentEmail = null)
+ {
+ $sentEmailRepo = $this->createMock(SentEmailRepository::class);
+ if (null !== $id) {
+ $sentEmailRepo->expects($this->once())->method('findOneBy')
+ ->with(['supportTicket' => $id])->willReturn($sentEmail);
+ }
+
+ return $sentEmailRepo;
+ }
+
+ /**
+ * @return MockObject|PhoneVerificationServiceInterface
+ */
+ private function getPhoneVerificationService()
+ {
+ return $this->createMock(PhoneVerificationServiceInterface::class);
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Tests/Twig/SupportExtensionsTest.php b/src/LoginCidadao/SupportBundle/Tests/Twig/SupportExtensionsTest.php
new file mode 100644
index 000000000..2bc9013f5
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Tests/Twig/SupportExtensionsTest.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Tests\Twig;
+
+use LoginCidadao\SupportBundle\Twig\SupportExtensions;
+use PHPUnit\Framework\TestCase;
+use Twig\TwigFilter;
+
+class SupportExtensionsTest extends TestCase
+{
+ public function testMask()
+ {
+ $ext = new SupportExtensions();
+
+ $filters = $ext->getFilters();
+
+ /** @var TwigFilter $filter */
+ $filter = $filters[0];
+
+ $this->assertCount(1, $filters);
+ $this->assertInstanceOf(TwigFilter::class, $filter);
+ $this->assertEquals('support_mask', $filter->getName());
+
+ $this->assertEquals('test', $ext->mask('test', false));
+ $this->assertEquals('****', $ext->mask('test', true));
+ $this->assertEquals('*', $ext->mask('test', true, 1));
+ $this->assertEquals('****, *****.', $ext->mask('Test, hello.', true));
+ $this->assertEquals('*****', $ext->mask('12345', true));
+ $this->assertEquals('****@*******.***', $ext->mask('mask@example.com', true));
+ }
+}
diff --git a/src/LoginCidadao/SupportBundle/Twig/SupportExtensions.php b/src/LoginCidadao/SupportBundle/Twig/SupportExtensions.php
new file mode 100644
index 000000000..accf67141
--- /dev/null
+++ b/src/LoginCidadao/SupportBundle/Twig/SupportExtensions.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace LoginCidadao\SupportBundle\Twig;
+
+use Twig\Extension\AbstractExtension;
+use Twig\TwigFilter;
+
+class SupportExtensions extends AbstractExtension
+{
+ public function getFilters()
+ {
+ return [
+ new TwigFilter('support_mask', [$this, 'mask']),
+ ];
+ }
+
+ public function mask($value, bool $mask = true, $hideLength = false)
+ {
+ if (!$mask) {
+ return $value;
+ }
+
+ if (!$hideLength) {
+ return preg_replace('/\w/', '*', $value);
+ } else {
+ return str_repeat('*', $hideLength);
+ }
+ }
+}
diff --git a/src/PROCERGS/LoginCidadao/CoreBundle/Helper/MeuRSHelper.php b/src/PROCERGS/LoginCidadao/CoreBundle/Helper/MeuRSHelper.php
index d0d58ed5e..97df5fa34 100644
--- a/src/PROCERGS/LoginCidadao/CoreBundle/Helper/MeuRSHelper.php
+++ b/src/PROCERGS/LoginCidadao/CoreBundle/Helper/MeuRSHelper.php
@@ -10,7 +10,7 @@
namespace PROCERGS\LoginCidadao\CoreBundle\Helper;
-use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use LoginCidadao\CoreBundle\Entity\PersonRepository;
use LoginCidadao\CoreBundle\Model\PersonInterface;
@@ -19,38 +19,32 @@
class MeuRSHelper
{
- /** @var EntityManager */
+ /** @var EntityManagerInterface */
protected $em;
/** @var PersonMeuRSRepository */
protected $personMeuRSRepository;
- public function __construct(
- EntityManager $em,
- EntityRepository $personMeuRSRepository
- ) {
+ public function __construct(EntityManagerInterface $em, EntityRepository $personMeuRSRepository)
+ {
$this->em = $em;
$this->personMeuRSRepository = $personMeuRSRepository;
}
/**
- * @param \LoginCidadao\CoreBundle\Model\PersonInterface $person
+ * @param PersonInterface $person
* @param boolean $create
* @return PersonMeuRS
*/
public function getPersonMeuRS(PersonInterface $person, $create = false)
{
- $personMeuRS = $this->personMeuRSRepository->findOneBy(
- array(
- 'person' => $person,
- )
- );
+ $personMeuRS = $this->personMeuRSRepository->findOneBy(['person' => $person]);
if ($create && !($personMeuRS instanceof PersonMeuRS)) {
$personMeuRS = new PersonMeuRS();
$personMeuRS->setPerson($person);
$this->em->persist($personMeuRS);
- $this->em->flush($personMeuRS);
+ $this->em->flush();
}
return $personMeuRS;
@@ -63,11 +57,8 @@ public function getVoterRegistration(PersonInterface $person)
return $personMeuRS ? $personMeuRS->getVoterRegistration() : null;
}
- public function setVoterRegistration(
- EntityManager $em,
- PersonInterface $person,
- $value
- ) {
+ public function setVoterRegistration(EntityManagerInterface $em, PersonInterface $person, $value)
+ {
$personMeuRS = $this->getPersonMeuRS($person, true);
$personMeuRS->setVoterRegistration($value);
diff --git a/src/PROCERGS/LoginCidadao/CoreBundle/Service/SupportHandlerDecorator.php b/src/PROCERGS/LoginCidadao/CoreBundle/Service/SupportHandlerDecorator.php
new file mode 100644
index 000000000..21d816f3c
--- /dev/null
+++ b/src/PROCERGS/LoginCidadao/CoreBundle/Service/SupportHandlerDecorator.php
@@ -0,0 +1,81 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace PROCERGS\LoginCidadao\CoreBundle\Service;
+
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Entity\SentEmail;
+use LoginCidadao\CoreBundle\Model\IdentifiablePersonInterface;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\SupportBundle\Model\SupportPerson;
+use LoginCidadao\SupportBundle\Service\SupportHandlerInterface;
+use PROCERGS\LoginCidadao\CoreBundle\Helper\MeuRSHelper;
+
+class SupportHandlerDecorator implements SupportHandlerInterface
+{
+ /** @var SupportHandlerInterface */
+ private $parent;
+
+ /** @var MeuRSHelper */
+ private $rsHelper;
+
+ /** @var PersonRepository */
+ private $personRepo;
+
+ /**
+ * SupportHandlerDecorator constructor.
+ * @param SupportHandlerInterface $parent
+ * @param MeuRSHelper $rsHelper
+ * @param PersonRepository $personRepo
+ */
+ public function __construct(SupportHandlerInterface $parent, MeuRSHelper $rsHelper, PersonRepository $personRepo)
+ {
+ $this->parent = $parent;
+ $this->rsHelper = $rsHelper;
+ $this->personRepo = $personRepo;
+ }
+
+ public function getThirdPartyConnections(IdentifiablePersonInterface $person): array
+ {
+ if (!$person instanceof PersonInterface) {
+ $person = $this->personRepo->find($person->getId());
+ }
+
+ $connections = $this->parent->getThirdPartyConnections($person);
+
+ if ($person instanceof PersonInterface) {
+ $personRS = $this->rsHelper->getPersonMeuRS($person, true);
+
+ $connections['nfg'] = $personRS->getNfgAccessToken() !== null;
+ }
+
+ return $connections;
+ }
+
+ public function getSupportPerson($id): SupportPerson
+ {
+ return $this->parent->getSupportPerson($id);
+ }
+
+ public function getPhoneMetadata(IdentifiablePersonInterface $person): array
+ {
+ return $this->parent->getPhoneMetadata($person);
+ }
+
+ public function getInitialMessage($ticket): ?SentEmail
+ {
+ return $this->parent->getInitialMessage($ticket);
+ }
+
+ public function getValidationMap(SupportPerson $person): array
+ {
+ return $this->parent->getValidationMap($person);
+ }
+}
diff --git a/src/PROCERGS/LoginCidadao/CoreBundle/Tests/Service/SupportHandlerDecoratorTest.php b/src/PROCERGS/LoginCidadao/CoreBundle/Tests/Service/SupportHandlerDecoratorTest.php
new file mode 100644
index 000000000..f39fe727e
--- /dev/null
+++ b/src/PROCERGS/LoginCidadao/CoreBundle/Tests/Service/SupportHandlerDecoratorTest.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace PROCERGS\LoginCidadao\CoreBundle\Tests\Service;
+
+use LoginCidadao\CoreBundle\Entity\PersonRepository;
+use LoginCidadao\CoreBundle\Model\IdentifiablePersonInterface;
+use LoginCidadao\CoreBundle\Model\PersonInterface;
+use LoginCidadao\SupportBundle\Model\SupportPerson;
+use LoginCidadao\SupportBundle\Service\SupportHandlerInterface;
+use PHPUnit\Framework\TestCase;
+use PROCERGS\LoginCidadao\CoreBundle\Entity\PersonMeuRS;
+use PROCERGS\LoginCidadao\CoreBundle\Service\SupportHandlerDecorator;
+use PROCERGS\LoginCidadao\CoreBundle\Helper\MeuRSHelper;
+
+class SupportHandlerDecoratorTest extends TestCase
+{
+ public function testGetThirdPartyConnections()
+ {
+ $identifiablePerson = $this->createMock(IdentifiablePersonInterface::class);
+ $identifiablePerson->expects($this->once())->method('getId')->willReturn($id = 321);
+
+ $person = $this->createMock(PersonInterface::class);
+
+ $personRS = $this->createMock(PersonMeuRS::class);
+ $personRS->expects($this->once())->method('getNfgAccessToken')->willReturn('123');
+
+ $supportHandler = $this->createMock(SupportHandlerInterface::class);
+ $supportHandler->expects($this->once())->method('getThirdPartyConnections')->with($person);
+
+ $rsHelper = $this->createMock(MeuRSHelper::class);
+ $rsHelper->expects($this->once())->method('getPersonMeuRS')->willReturn($personRS);
+ $personRepo = $this->createMock(PersonRepository::class);
+ $personRepo->expects($this->once())->method('find')->with($id)->willReturn($person);
+
+ $decorator = new SupportHandlerDecorator($supportHandler, $rsHelper, $personRepo);
+ $decorator->getThirdPartyConnections($identifiablePerson);
+ }
+
+ public function testDecorator()
+ {
+ $id = 123;
+ $idPerson = $this->createMock(IdentifiablePersonInterface::class);
+ $ticket = 'random_ticket';
+ $supportPerson = $this->createMock(SupportPerson::class);
+
+ $supportHandler = $this->createMock(SupportHandlerInterface::class);
+ $supportHandler->expects($this->once())->method('getSupportPerson')->with($id);
+ $supportHandler->expects($this->once())->method('getPhoneMetadata')->with($idPerson);
+ $supportHandler->expects($this->once())->method('getInitialMessage')->with($ticket);
+ $supportHandler->expects($this->once())->method('getValidationMap')->with($supportPerson);
+
+ $decorator = new SupportHandlerDecorator($supportHandler, $this->getRsHelper(), $this->getPersonRepo());
+ $decorator->getSupportPerson($id);
+ $decorator->getPhoneMetadata($idPerson);
+ $decorator->getInitialMessage($ticket);
+ $decorator->getValidationMap($supportPerson);
+ }
+
+ private function getRsHelper()
+ {
+ return $this->createMock(MeuRSHelper::class);
+ }
+
+ private function getPersonRepo()
+ {
+ return $this->createMock(PersonRepository::class);
+ }
+}