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/Resources/views/Admin/sidebar.html.twig b/src/LoginCidadao/CoreBundle/Resources/views/Admin/sidebar.html.twig index c091f60bd..364c23973 100644 --- a/src/LoginCidadao/CoreBundle/Resources/views/Admin/sidebar.html.twig +++ b/src/LoginCidadao/CoreBundle/Resources/views/Admin/sidebar.html.twig @@ -34,6 +34,14 @@ + {% if is_granted(['FEATURE_PHONE_BLOCKLIST', 'ROLE_EDIT_BLOCKED_PHONES']) %} +
  • + + {{ 'admin.blocklist.menu_item' | trans }} + + +
  • + {% endif %} {% if is_granted('ROLE_AUDIT_PROFILE_VIEWS') %}
  • diff --git a/src/LoginCidadao/CoreBundle/Resources/views/Person/accountAutoBlocked.html.twig b/src/LoginCidadao/CoreBundle/Resources/views/Person/accountAutoBlocked.html.twig new file mode 100644 index 000000000..3a9f88472 --- /dev/null +++ b/src/LoginCidadao/CoreBundle/Resources/views/Person/accountAutoBlocked.html.twig @@ -0,0 +1,20 @@ +{% block subject %} + {% autoescape false %} + {{ 'email.account_auto_blocked.message.subject' | trans }} + {% endautoescape %} +{% endblock %} + +{% block body_text %} + {% autoescape false %} + {{ 'email.account_auto_blocked.message.text' | trans({ '%email%': email }) }} + {% endautoescape %} +{% endblock %} + +{% block body_html %} + {% autoescape false %} + {{ include("LoginCidadaoCoreBundle::common.email.html.twig", { + 'subject' : 'email.account_auto_blocked.message.subject' | trans, + 'msg' : 'email.account_auto_blocked.message.html' | trans({ '%email%': email }) | raw + } ) }} + {% endautoescape %} +{% endblock %} diff --git a/src/LoginCidadao/CoreBundle/Resources/views/sidebar.html.twig b/src/LoginCidadao/CoreBundle/Resources/views/sidebar.html.twig index c54e46e72..9f81ccc48 100644 --- a/src/LoginCidadao/CoreBundle/Resources/views/sidebar.html.twig +++ b/src/LoginCidadao/CoreBundle/Resources/views/sidebar.html.twig @@ -81,6 +81,14 @@
  • {% endif %} + {% if app.user is not null and is_granted('ROLE_SUPPORT_SEARCH_USERS') %} +
  • + + {{ 'support.search.menu.support' | trans }} + + +
  • + {% endif %} {% endblock %}
    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 }}

    +
    + +
    +
    +
    +
    +
    + {{ form(form) }} +
    +
    +
    +
    + + {% 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 }}

    +
    +
    +
    +
    + {{ form(form) }} +
    +
    +
    +
    +
    +{% 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.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.warning' | trans }}

    + {{ 'tasks.verify_phone.skip.confirm' | 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 +
    {{ '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 }}

    +
    +
    +
    +
    +
    + {% for validation in dataValidation %} +
    + +
    +
    + +
    + {% endfor %} +
    +
    +
    +
    +
    + {% 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); + } +}