mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-12 13:24:25 +00:00
allow users to recover password
This commit is contained in:
parent
5b1ba71a8a
commit
1fd6a4ed2c
@ -10,6 +10,12 @@ use Chill\MainBundle\Entity\User;
|
|||||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
use Symfony\Component\Translation\TranslatorInterface;
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Validator\Constraints\Callback;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
use Chill\MainBundle\Security\PasswordRecover\RecoverPasswordHelper;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Chill\MainBundle\Security\PasswordRecover\TokenManager;
|
||||||
|
|
||||||
class PasswordController extends Controller
|
class PasswordController extends Controller
|
||||||
{
|
{
|
||||||
@ -31,14 +37,30 @@ class PasswordController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected $chillLogger;
|
protected $chillLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var RecoverPasswordHelper
|
||||||
|
*/
|
||||||
|
protected $recoverPasswordHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var TokenManager
|
||||||
|
*/
|
||||||
|
protected $tokenManager;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
LoggerInterface $chillLogger,
|
LoggerInterface $chillLogger,
|
||||||
UserPasswordEncoderInterface $passwordEncoder,
|
UserPasswordEncoderInterface $passwordEncoder,
|
||||||
|
RecoverPasswordHelper $recoverPasswordHelper,
|
||||||
|
TokenManager $tokenManager,
|
||||||
TranslatorInterface $translator
|
TranslatorInterface $translator
|
||||||
) {
|
) {
|
||||||
$this->chillLogger = $chillLogger;
|
$this->chillLogger = $chillLogger;
|
||||||
$this->passwordEncoder = $passwordEncoder;
|
$this->passwordEncoder = $passwordEncoder;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
|
$this->tokenManager = $tokenManager;
|
||||||
|
$this->recoverPasswordHelper = $recoverPasswordHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,14 +119,148 @@ class PasswordController extends Controller
|
|||||||
*/
|
*/
|
||||||
private function passwordForm(User $user)
|
private function passwordForm(User $user)
|
||||||
{
|
{
|
||||||
return $this->createForm(
|
return $this
|
||||||
|
->createForm(
|
||||||
UserPasswordType::class,
|
UserPasswordType::class,
|
||||||
[],
|
[],
|
||||||
[ 'user' => $this->getUser() ]
|
[ 'user' => $user ]
|
||||||
)
|
)
|
||||||
->add('submit', SubmitType::class, array('label' => 'Change password'))
|
->add('submit', SubmitType::class, array('label' => 'Change password'))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function recoverAction(Request $request)
|
||||||
|
{
|
||||||
|
$query = $request->query;
|
||||||
|
$username = $query->get(TokenManager::USERNAME_CANONICAL);
|
||||||
|
$hash = $query->getAlnum(TokenManager::HASH);
|
||||||
|
$token = $query->getAlnum(TokenManager::TOKEN);
|
||||||
|
$timestamp = $query->getInt(TokenManager::TIMESTAMP);
|
||||||
|
$user = $this->getDoctrine()->getRepository(User::class)
|
||||||
|
->findOneByUsernameCanonical($username);
|
||||||
|
|
||||||
|
if (NULL === $user) {
|
||||||
|
throw $this->createNotFoundException(sprintf('User %s not found', $username));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TRUE !== $this->tokenManager->verify($hash, $token, $user, $timestamp)) {
|
||||||
|
return new Response("Invalid token", Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->passwordForm($user);
|
||||||
|
$form->remove('actual_password');
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$password = $form->get('new_password')->getData();
|
||||||
|
$user->setPassword($this->passwordEncoder->encodePassword($user, $password));
|
||||||
|
// logging for prod
|
||||||
|
$this
|
||||||
|
->chillLogger
|
||||||
|
->notice(
|
||||||
|
'setting new password for user',
|
||||||
|
array(
|
||||||
|
'user' => $user->getUsername()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->getDoctrine()->getManager()->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('password_request_recover_changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@ChillMain/Password/recover_password_form.html.twig', [
|
||||||
|
'form' => $form->createView()
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changeConfirmedAction()
|
||||||
|
{
|
||||||
|
return $this->render('@ChillMain/Password/recover_password_changed.html.twig');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requestRecoverAction(Request $request)
|
||||||
|
{
|
||||||
|
$form = $this->requestRecoverForm();
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
/* @var $qb \Doctrine\ORM\QueryBuilder */
|
||||||
|
$qb = $this->getDoctrine()->getManager()
|
||||||
|
->createQueryBuilder();
|
||||||
|
$qb->select('u')
|
||||||
|
->from(User::class, 'u')
|
||||||
|
->where($qb->expr()->eq('u.usernameCanonical', 'UNACCENT(LOWER(:pattern))'))
|
||||||
|
->orWhere($qb->expr()->eq('u.emailCanonical', 'UNACCENT(LOWER(:pattern))' ))
|
||||||
|
->setParameter('pattern', $form->get('username_or_email')->getData())
|
||||||
|
;
|
||||||
|
|
||||||
|
$user = $qb->getQuery()->getSingleResult();
|
||||||
|
|
||||||
|
if (empty($user->getEmail())) {
|
||||||
|
$this->addFlash('error', $this->translator->trans('This account does not have an email address. '
|
||||||
|
. 'Please ask your administrator to renew your password.'));
|
||||||
|
} else {
|
||||||
|
$this->recoverPasswordHelper->sendRecoverEmail($user,
|
||||||
|
(new \DateTimeImmutable('now'))->add(new \DateInterval('PT30M')));
|
||||||
|
|
||||||
|
// logging for prod
|
||||||
|
$this
|
||||||
|
->chillLogger
|
||||||
|
->notice(
|
||||||
|
'Sending an email for password recovering',
|
||||||
|
array(
|
||||||
|
'user' => $user->getUsername()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('password_request_recover_confirm');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@ChillMain/Password/request_recover_password.html.twig', [
|
||||||
|
'form' => $form->createView()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requestRecoverConfirmAction()
|
||||||
|
{
|
||||||
|
return $this->render('@ChillMain/Password/request_recover_password_confirm.html.twig');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function requestRecoverForm()
|
||||||
|
{
|
||||||
|
$builder = $this->createFormBuilder();
|
||||||
|
$builder
|
||||||
|
->add('username_or_email', TextType::class, [
|
||||||
|
'label' => 'Username or email',
|
||||||
|
'constraints' => [
|
||||||
|
new Callback([
|
||||||
|
'callback' => function($pattern, ExecutionContextInterface $context, $payload) {
|
||||||
|
$qb = $this->getDoctrine()->getManager()
|
||||||
|
->createQueryBuilder();
|
||||||
|
$qb->select('COUNT(u)')
|
||||||
|
->from(User::class, 'u')
|
||||||
|
->where($qb->expr()->eq('u.usernameCanonical', 'UNACCENT(LOWER(:pattern))'))
|
||||||
|
->orWhere($qb->expr()->eq('u.emailCanonical', 'UNACCENT(LOWER(:pattern))' ))
|
||||||
|
->setParameter('pattern', $pattern)
|
||||||
|
;
|
||||||
|
|
||||||
|
if ((int) $qb->getQuery()->getSingleScalarResult() !== 1) {
|
||||||
|
$context->addViolation('This username or email does not exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->add('submit', SubmitType::class, [
|
||||||
|
'label' => 'Request recover'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $builder->getForm();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,9 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
|
|||||||
$container->setParameter('chill_main.pagination.item_per_page',
|
$container->setParameter('chill_main.pagination.item_per_page',
|
||||||
$config['pagination']['item_per_page']);
|
$config['pagination']['item_per_page']);
|
||||||
|
|
||||||
|
$container->setParameter('chill_main.notifications',
|
||||||
|
$config['notifications']);
|
||||||
|
|
||||||
// add the key 'widget' without the key 'enable'
|
// add the key 'widget' without the key 'enable'
|
||||||
$container->setParameter('chill_main.widgets',
|
$container->setParameter('chill_main.widgets',
|
||||||
isset($config['widgets']['homepage']) ?
|
isset($config['widgets']['homepage']) ?
|
||||||
@ -100,6 +103,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
|
|||||||
$loader->load('services/fixtures.yml');
|
$loader->load('services/fixtures.yml');
|
||||||
$loader->load('services/menu.yml');
|
$loader->load('services/menu.yml');
|
||||||
$loader->load('services/security.yml');
|
$loader->load('services/security.yml');
|
||||||
|
$loader->load('services/notification.yml');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getConfiguration(array $config, ContainerBuilder $container)
|
public function getConfiguration(array $config, ContainerBuilder $container)
|
||||||
|
@ -67,6 +67,24 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end() // end of integer 'item_per_page'
|
->end() // end of integer 'item_per_page'
|
||||||
->end() // end of children
|
->end() // end of children
|
||||||
->end() // end of pagination
|
->end() // end of pagination
|
||||||
|
->arrayNode('notifications')
|
||||||
|
->children()
|
||||||
|
->scalarNode('from_email')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->scalarNode('from_name')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->enumNode('scheme')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->values(['http', 'https'])
|
||||||
|
->defaultValue('https')
|
||||||
|
->end()
|
||||||
|
->scalarNode('host')
|
||||||
|
->cannotBeEmpty()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end() // end of notifications
|
||||||
->arrayNode('widgets')
|
->arrayNode('widgets')
|
||||||
->canBeEnabled()
|
->canBeEnabled()
|
||||||
->canBeUnset()
|
->canBeUnset()
|
||||||
|
168
Notification/Mailer.php
Normal file
168
Notification/Mailer.php
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
namespace Chill\MainBundle\Notification;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe d'aide pour l'envoi de notification.
|
||||||
|
*
|
||||||
|
* Héberge toutes les méthodes pour ré-écrire les URL en fonction de la langue
|
||||||
|
* de l'utilisateur.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Mailer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var LoggerInterface
|
||||||
|
*/
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var \Twig\Environment
|
||||||
|
*/
|
||||||
|
protected $twig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var \Swift_Mailer
|
||||||
|
*/
|
||||||
|
protected $mailer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var \Swift_Mailer
|
||||||
|
*/
|
||||||
|
protected $forcedMailer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var RouterInterface
|
||||||
|
*/
|
||||||
|
protected $router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
protected $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $routeParameters;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
LoggerInterface $logger,
|
||||||
|
\Twig\Environment $twig,
|
||||||
|
\Swift_Mailer $mailer,
|
||||||
|
// due to bug https://github.com/symfony/swiftmailer-bundle/issues/127
|
||||||
|
\Swift_Transport $mailerTransporter,
|
||||||
|
RouterInterface $router,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
$routeParameters
|
||||||
|
) {
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->twig = $twig;
|
||||||
|
$this->mailer = $mailer;
|
||||||
|
$this->router = $router;
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->routeParameters = $routeParameters;
|
||||||
|
$this->forcedMailer = new \Swift_Mailer($mailerTransporter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie une notification à un utilisateur.
|
||||||
|
*
|
||||||
|
* @param \User $to
|
||||||
|
* @param array $subject Subject of the message [ 0 => $message (required), 1 => $parameters (optional), 3 => $domain (optional) ]
|
||||||
|
* @param array $bodies The bodies. An array where keys are the contentType and values the bodies
|
||||||
|
* @param \callable $callback a callback to customize the message (add attachment, etc.)
|
||||||
|
*/
|
||||||
|
public function sendNotification(
|
||||||
|
$recipient,
|
||||||
|
array $subject,
|
||||||
|
array $bodies,
|
||||||
|
callable $callback = null,
|
||||||
|
$force = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$fromEmail = $this->routeParameters['from_email'];
|
||||||
|
$fromName = $this->routeParameters['from_name'];
|
||||||
|
$to = $recipient instanceof User ? $recipient->getEmail() : $recipient;
|
||||||
|
|
||||||
|
$subjectI18n = $this->translator->trans(
|
||||||
|
$subject[0],
|
||||||
|
$subject[1] ?? [],
|
||||||
|
$subject[2] ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = (new \Swift_Message($subjectI18n))
|
||||||
|
->setFrom($fromEmail, $fromName)
|
||||||
|
->setTo($to)
|
||||||
|
;
|
||||||
|
|
||||||
|
foreach ($bodies as $contentType => $content) {
|
||||||
|
$message->setBody($content, $contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($callback !== null) {
|
||||||
|
\call_user_func($callback, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info("[notification] Sending notification", [
|
||||||
|
'to' => $message->getTo(),
|
||||||
|
'subject' => $message->getSubject()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->sendMessage($message, $force);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendMessage(\Swift_Message $message, $force)
|
||||||
|
{
|
||||||
|
if ($force) {
|
||||||
|
$this->forcedMailer->send($message);
|
||||||
|
} else {
|
||||||
|
$this->mailer->send($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderContentToUser(User $to, $template, array $parameters = array())
|
||||||
|
{
|
||||||
|
$context = $this->router->getContext();
|
||||||
|
$previousHost = $context->getHost();
|
||||||
|
$previousScheme = $context->getScheme();
|
||||||
|
|
||||||
|
$context->setHost($this->routeParameters['host']);
|
||||||
|
$context->setScheme($this->routeParameters['scheme']);
|
||||||
|
|
||||||
|
$content = $this->twig->render($template, $parameters);
|
||||||
|
|
||||||
|
// reset the host
|
||||||
|
$context->setHost($previousHost);
|
||||||
|
$context->setScheme($previousScheme);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,14 @@ chill_main_exports:
|
|||||||
chill_postal_code:
|
chill_postal_code:
|
||||||
resource: "@ChillMainBundle/Resources/config/routing/postal-code.yml"
|
resource: "@ChillMainBundle/Resources/config/routing/postal-code.yml"
|
||||||
prefix: "{_locale}/postal-code"
|
prefix: "{_locale}/postal-code"
|
||||||
|
|
||||||
|
chill_password:
|
||||||
|
resource: "@ChillMainBundle/Resources/config/routing/password.yml"
|
||||||
|
prefix: "{_locale}/password"
|
||||||
|
|
||||||
|
chill_password_recover:
|
||||||
|
resource: "@ChillMainBundle/Resources/config/routing/password_recover.yml"
|
||||||
|
prefix: "public/{_locale}/password"
|
||||||
|
|
||||||
root:
|
root:
|
||||||
path: /
|
path: /
|
||||||
@ -78,7 +86,3 @@ login_check:
|
|||||||
|
|
||||||
logout:
|
logout:
|
||||||
path: /logout
|
path: /logout
|
||||||
|
|
||||||
change_my_password:
|
|
||||||
path: /{_locale}/password/edit
|
|
||||||
defaults: { _controller: ChillMainBundle:Password:userPassword }
|
|
4
Resources/config/routing/password.yml
Normal file
4
Resources/config/routing/password.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
change_my_password:
|
||||||
|
path: /edit
|
||||||
|
defaults: { _controller: ChillMainBundle:Password:userPassword }
|
||||||
|
|
15
Resources/config/routing/password_recover.yml
Normal file
15
Resources/config/routing/password_recover.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
password_recover:
|
||||||
|
path: /recover
|
||||||
|
defaults: { _controller: ChillMainBundle:Password:recover }
|
||||||
|
|
||||||
|
password_request_recover:
|
||||||
|
path: /request-recover
|
||||||
|
defaults: { _controller: ChillMainBundle:Password:requestRecover }
|
||||||
|
|
||||||
|
password_request_recover_confirm:
|
||||||
|
path: /request-confirm
|
||||||
|
defaults: { _controller: ChillMainBundle:Password:requestRecoverConfirm }
|
||||||
|
|
||||||
|
password_request_recover_changed:
|
||||||
|
path: /request-changed
|
||||||
|
defaults: { _controller: ChillMainBundle:Password:changeConfirmed }
|
10
Resources/config/services/notification.yml
Normal file
10
Resources/config/services/notification.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
Chill\MainBundle\Notification\Mailer:
|
||||||
|
arguments:
|
||||||
|
- "@logger"
|
||||||
|
- "@twig"
|
||||||
|
- "@mailer"
|
||||||
|
- "@swiftmailer.transport"
|
||||||
|
- "@router"
|
||||||
|
- "@translator"
|
||||||
|
- "%chill_main.notifications%"
|
@ -20,4 +20,15 @@ services:
|
|||||||
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
|
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
|
||||||
tags:
|
tags:
|
||||||
- { name: security.voter }
|
- { name: security.voter }
|
||||||
|
|
||||||
|
Chill\MainBundle\Security\PasswordRecover\TokenManager:
|
||||||
|
arguments:
|
||||||
|
$secret: 'secret'
|
||||||
|
$logger: '@Psr\Log\LoggerInterface'
|
||||||
|
|
||||||
|
Chill\MainBundle\Security\PasswordRecover\RecoverPasswordHelper:
|
||||||
|
arguments:
|
||||||
|
$tokenManager: '@Chill\MainBundle\Security\PasswordRecover\TokenManager'
|
||||||
|
$urlGenerator: '@Symfony\Component\Routing\Generator\UrlGeneratorInterface'
|
||||||
|
$mailer: '@Chill\MainBundle\Notification\Mailer'
|
||||||
|
|
@ -43,7 +43,7 @@ label {
|
|||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
|
color: 'black';
|
||||||
}
|
}
|
||||||
form {
|
form {
|
||||||
|
|
||||||
@ -61,3 +61,12 @@ button {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.forgot-password-link {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 300;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
@ -189,6 +189,22 @@ Choose the format: Choisir le format
|
|||||||
'select2.error_loading': Erreur de chargement des résultats
|
'select2.error_loading': Erreur de chargement des résultats
|
||||||
'select2.searching': Recherche en cours...
|
'select2.searching': Recherche en cours...
|
||||||
|
|
||||||
# page changement mot de passe
|
# change password
|
||||||
Change my password: Modification du mot de passe
|
Change my password: Modification du mot de passe
|
||||||
Your actual password: Mot de passe actuel
|
Your actual password: Mot de passe actuel
|
||||||
|
|
||||||
|
# recover password
|
||||||
|
Forgot your password ?: Mot de passe oublié ?
|
||||||
|
Recover password: Remplacement du mot de passe
|
||||||
|
Username or email: Nom d'utilisateur ou email
|
||||||
|
Request recover: Demande de remplacement
|
||||||
|
Check your email: Vérifiez votre courriel
|
||||||
|
An email has been sent to your address. Click on the link inside this email to confirm that you are the owner of this account.: Un courriel a été envoyé à votre adresse. Cliquez sur le lien de cet email pour confirmer que vous êtes bien le propriétaire de ce compte.
|
||||||
|
You requested to recover your password: Vous avez demandé à renouveler votre mot de passe.
|
||||||
|
Click on the link below to recover your password: Cliquez sur le lien ci-dessous pour re-générer votre mot de passe
|
||||||
|
Regards,: Cordialement,
|
||||||
|
Your administrator: Votre administrateur
|
||||||
|
Recover your password: Regénération du mot de passe
|
||||||
|
New password set: Le nouveau mot de passe est enregistré
|
||||||
|
Your password has been set.: Votre mot de passe a été changé.
|
||||||
|
Log in with your new password: Connectez-vous avec votre nouveau mot de passe
|
@ -4,6 +4,7 @@ The role "%role%" require to be associated with a scope.: Le rôle "%role%" doit
|
|||||||
The role "%role%" should not be associated with a scope.: Le rôle "%role%" ne doit pas être associé à un cercle.
|
The role "%role%" should not be associated with a scope.: Le rôle "%role%" ne doit pas être associé à un cercle.
|
||||||
"The password must contains one letter, one capitalized letter, one number and one special character as *[@#$%!,;:+\"'-/{}~=µ()£]). Other characters are allowed.": "Le mot de passe doit contenir une majuscule, une minuscule, et au moins un caractère spécial parmi *[@#$%!,;:+\"'-/{}~=µ()£]). Les autres caractères sont autorisés."
|
"The password must contains one letter, one capitalized letter, one number and one special character as *[@#$%!,;:+\"'-/{}~=µ()£]). Other characters are allowed.": "Le mot de passe doit contenir une majuscule, une minuscule, et au moins un caractère spécial parmi *[@#$%!,;:+\"'-/{}~=µ()£]). Les autres caractères sont autorisés."
|
||||||
The password fields must match: Les mots de passe doivent correspondre
|
The password fields must match: Les mots de passe doivent correspondre
|
||||||
|
The password must be greater than {{ limit }} characters: "[1,Inf] Le mot de passe doit contenir au moins {{ limit }} caractères"
|
||||||
|
|
||||||
A permission is already present for the same role and scope: Une permission est déjà présente pour le même rôle et cercle.
|
A permission is already present for the same role and scope: Une permission est déjà présente pour le même rôle et cercle.
|
||||||
|
|
||||||
|
@ -42,9 +42,11 @@
|
|||||||
<input type="password" name="_password" />
|
<input type="password" name="_password" />
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" />
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" />
|
||||||
<br/>
|
<br/>
|
||||||
<button type="submit" name="login">{{ 'Login'|trans }}</button>
|
<button type="submit" name="login">{{ 'Login'|trans }}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<p class="forgot-password-link"><a href="{{ path('password_request_recover') }}">{{ 'Forgot your password ?'|trans }}</a></p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
10
Resources/views/Password/recover_email.txt.twig
Normal file
10
Resources/views/Password/recover_email.txt.twig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{{ user.username }},
|
||||||
|
|
||||||
|
{{ 'You requested to recover your password'|trans }}
|
||||||
|
|
||||||
|
{{ 'Click on the link below to recover your password'|trans }} :
|
||||||
|
|
||||||
|
{{ url|raw }}
|
||||||
|
|
||||||
|
{{ 'Regards,'|trans }}
|
||||||
|
{{ 'Your administrator'|trans }}
|
49
Resources/views/Password/recover_layout.html.twig
Normal file
49
Resources/views/Password/recover_layout.html.twig
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{#
|
||||||
|
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||||
|
<info@champs-libres.coop> / <http://www.champs-libres.coop>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>
|
||||||
|
{{ 'Login to %installation_name%' | trans({ '%installation_name%' : installation.name } ) }}
|
||||||
|
</title>
|
||||||
|
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="{{ asset('build/chill.css') }}"/>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="navigation container">
|
||||||
|
<div class="grid-4 hide-tablet hide-mobile parent">
|
||||||
|
<div class="grid-10 push-2 grid-tablet-12 grid-mobile-12 push-tablet-0 grid-mobile-0 logo-container">
|
||||||
|
<a href="{{ path('chill_main_homepage') }}">
|
||||||
|
<img class="logo" src="{{ asset('build/images/logo-chill-sans-slogan_white.png') }}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
Resources/views/Password/recover_password_changed.html.twig
Normal file
14
Resources/views/Password/recover_password_changed.html.twig
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "@ChillMain/Password/recover_layout.html.twig" %}
|
||||||
|
|
||||||
|
{% block title %}{{ "New password set"|trans }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid-10 grid-mobile-10 grid-tablet-10 centered">
|
||||||
|
|
||||||
|
<h1>{{ "New password set"|trans }}</h1>
|
||||||
|
|
||||||
|
<p>{{ "Your password has been set."|trans }} <a href="{{ path('login') }}">{{ "Log in with your new password"|trans }}</a></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
25
Resources/views/Password/recover_password_form.html.twig
Normal file
25
Resources/views/Password/recover_password_form.html.twig
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "@ChillMain/Password/recover_layout.html.twig" %}
|
||||||
|
|
||||||
|
{% set title = app.request.get('title', "Recover your password"|trans) %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid-10 grid-mobile-10 grid-tablet-10 centered">
|
||||||
|
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{{ form_row(form.new_password) }}
|
||||||
|
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li>
|
||||||
|
{{ form_widget(form.submit, { 'attr': { 'class': 'sc-button orange' } } ) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
43
Resources/views/Password/request_recover_password.html.twig
Normal file
43
Resources/views/Password/request_recover_password.html.twig
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{#
|
||||||
|
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||||
|
<info@champs-libres.coop> / <http://www.champs-libres.coop>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
|
||||||
|
{% extends "@ChillMain/Password/recover_layout.html.twig" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}{{"Recover password"|trans}}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid-10 grid-mobile-10 grid-tablet-10 centered">
|
||||||
|
|
||||||
|
<h1>{{ 'Recover password'|trans }}</h1>
|
||||||
|
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{{ form_row(form.username_or_email) }}
|
||||||
|
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li>
|
||||||
|
{{ form_widget(form.submit, { 'attr': { 'class': 'sc-button orange' } } ) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "@ChillMain/Password/recover_layout.html.twig" %}
|
||||||
|
|
||||||
|
{% block title "Check your email"|trans %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="grid-10 grid-mobile-10 grid-tablet-10 centered">
|
||||||
|
|
||||||
|
<h1>{{ 'Check your email'|trans }}</h1>
|
||||||
|
|
||||||
|
<p>{{ 'An email has been sent to your address. Click on the link inside this email to confirm that you are the owner of this account.'|trans }}</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
98
Security/PasswordRecover/RecoverPasswordHelper.php
Normal file
98
Security/PasswordRecover/RecoverPasswordHelper.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
namespace Chill\MainBundle\Security\PasswordRecover;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Security\PasswordRecover\TokenManager;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Chill\MainBundle\Notification\Mailer;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||||
|
*/
|
||||||
|
class RecoverPasswordHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var TokenManager
|
||||||
|
*/
|
||||||
|
protected $tokenManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var UrlGeneratorInterface
|
||||||
|
*/
|
||||||
|
protected $urlGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var Mailer
|
||||||
|
*/
|
||||||
|
protected $mailer;
|
||||||
|
|
||||||
|
const RECOVER_PASSWORD_ROUTE = 'password_recover';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
TokenManager $tokenManager,
|
||||||
|
UrlGeneratorInterface $urlGenerator,
|
||||||
|
Mailer $mailer
|
||||||
|
) {
|
||||||
|
$this->tokenManager = $tokenManager;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
|
$this->mailer = $mailer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function generateUrl(User $user, \DateTimeInterface $expiration, $absolute = true)
|
||||||
|
{
|
||||||
|
return $this->urlGenerator->generate(
|
||||||
|
self::RECOVER_PASSWORD_ROUTE,
|
||||||
|
$this->tokenManager->generate($user, $expiration),
|
||||||
|
$absolute ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::ABSOLUTE_PATH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendRecoverEmail(
|
||||||
|
User $user,
|
||||||
|
\DateTimeInterface $expiration,
|
||||||
|
$template = '@ChillMain/Password/recover_email.txt.twig',
|
||||||
|
array $templateParameters = [],
|
||||||
|
$force = false
|
||||||
|
) {
|
||||||
|
$content = $this->mailer->renderContentToUser(
|
||||||
|
$user,
|
||||||
|
$template,
|
||||||
|
\array_merge([
|
||||||
|
'user' => $user,
|
||||||
|
'url' => $this->generateUrl($user, $expiration, true)
|
||||||
|
],
|
||||||
|
$templateParameters
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->mailer->sendNotification(
|
||||||
|
$user,
|
||||||
|
[ 'Recover your password' ],
|
||||||
|
[
|
||||||
|
'text/plain' => $content,
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
$force);
|
||||||
|
}
|
||||||
|
}
|
96
Security/PasswordRecover/TokenManager.php
Normal file
96
Security/PasswordRecover/TokenManager.php
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
namespace Chill\MainBundle\Security\PasswordRecover;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||||
|
*/
|
||||||
|
class TokenManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $secret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var LoggerInterface
|
||||||
|
*/
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
|
const TOKEN = 't';
|
||||||
|
const HASH = 'h';
|
||||||
|
const TIMESTAMP = 'ts';
|
||||||
|
const USERNAME_CANONICAL = 'u';
|
||||||
|
|
||||||
|
public function __construct($secret, LoggerInterface $logger)
|
||||||
|
{
|
||||||
|
$this->secret = $secret;
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(User $user, \DateTimeInterface $expiration)
|
||||||
|
{
|
||||||
|
$token = \random_bytes(32);
|
||||||
|
$username = $user->getUsernameCanonical();
|
||||||
|
|
||||||
|
if (empty($username)) {
|
||||||
|
throw new \UnexpectedValueException("username should not be empty to generate a token");
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = $expiration->getTimestamp();
|
||||||
|
$hash = \hash('sha512', $token.$username.$timestamp.$this->secret);
|
||||||
|
|
||||||
|
return [
|
||||||
|
self::HASH => $hash,
|
||||||
|
self::TOKEN => \bin2hex($token),
|
||||||
|
self::TIMESTAMP => $timestamp,
|
||||||
|
self::USERNAME_CANONICAL => $username
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verify($hash, $token, User $user, $timestamp)
|
||||||
|
{
|
||||||
|
$token = \hex2bin($token);
|
||||||
|
$username = $user->getUsernameCanonical();
|
||||||
|
$date = \DateTimeImmutable::createFromFormat('U', $timestamp);
|
||||||
|
|
||||||
|
if ($date < new \DateTime('now')) {
|
||||||
|
|
||||||
|
$this->logger->info('receiving a password recover token with expired '
|
||||||
|
. 'validity');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected = \hash('sha512', $token.$username.$timestamp.$this->secret);
|
||||||
|
|
||||||
|
if ($expected !== $hash) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
117
Tests/Security/PasswordRecover/TokenManagerTest.php
Normal file
117
Tests/Security/PasswordRecover/TokenManagerTest.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
namespace Chill\MainBundle\Tests\PasswordRecover;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Chill\MainBundle\Security\PasswordRecover\TokenManager;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||||
|
*/
|
||||||
|
class TokenManagerTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
protected $tokenManager;
|
||||||
|
|
||||||
|
public static function setUpBefore()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
|
||||||
|
$logger = self::$kernel
|
||||||
|
->getContainer()
|
||||||
|
->get('logger');
|
||||||
|
|
||||||
|
$this->tokenManager = new TokenManager('secret', $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerate()
|
||||||
|
{
|
||||||
|
$tokenManager = $this->tokenManager;
|
||||||
|
$user = (new User())->setUsernameCanonical('test');
|
||||||
|
$expiration = new \DateTimeImmutable('tomorrow');
|
||||||
|
|
||||||
|
$tokens = $tokenManager->generate($user, $expiration);
|
||||||
|
|
||||||
|
$this->assertInternalType('array', $tokens);
|
||||||
|
$this->assertArrayHasKey('h', $tokens);
|
||||||
|
$this->assertArrayHasKey('t', $tokens);
|
||||||
|
$this->assertNotEmpty($tokens['h']);
|
||||||
|
$this->assertNotEmpty($tokens['t']);
|
||||||
|
$this->assertEquals($user->getUsernameCanonical(), $tokens['u']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \UnexpectedValueException
|
||||||
|
*/
|
||||||
|
public function testGenerateEmptyUsernameCanonical()
|
||||||
|
{
|
||||||
|
$tokenManager = $this->tokenManager;
|
||||||
|
// set a username, but not a username canonical
|
||||||
|
$user = (new User())->setUsername('test');
|
||||||
|
$expiration = new \DateTimeImmutable('tomorrow');
|
||||||
|
|
||||||
|
$tokenManager->generate($user, $expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVerify()
|
||||||
|
{
|
||||||
|
$tokenManager = $this->tokenManager;
|
||||||
|
$user = (new User())->setUsernameCanonical('test');
|
||||||
|
$expiration = new \DateTimeImmutable('tomorrow');
|
||||||
|
|
||||||
|
$tokens = $tokenManager->generate($user, $expiration);
|
||||||
|
|
||||||
|
$hash = $tokens[TokenManager::HASH];
|
||||||
|
$token = $tokens[TokenManager::TOKEN];
|
||||||
|
$timestamp = $tokens[TokenManager::TIMESTAMP];
|
||||||
|
|
||||||
|
$verification = $tokenManager->verify($hash, $token, $user, $timestamp);
|
||||||
|
|
||||||
|
$this->assertTrue($verification);
|
||||||
|
|
||||||
|
// test with altering token
|
||||||
|
$this->assertFalse($tokenManager->verify($hash.'5', $token, $user, $timestamp));
|
||||||
|
$this->assertFalse($tokenManager->verify($hash, $token.'25', $user, $timestamp));
|
||||||
|
$this->assertFalse($tokenManager->verify($hash, $token, $user->setUsernameCanonical('test2'), $timestamp));
|
||||||
|
$this->assertFalse($tokenManager->verify($hash, $token, $user, $timestamp+1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVerifyExpiredFails()
|
||||||
|
{
|
||||||
|
$tokenManager = $this->tokenManager;
|
||||||
|
$user = (new User())->setUsernameCanonical('test');
|
||||||
|
$expiration = (new \DateTimeImmutable('now'))->sub(new \DateInterval('PT1S'));
|
||||||
|
|
||||||
|
$tokens = $tokenManager->generate($user, $expiration);
|
||||||
|
|
||||||
|
$hash = $tokens[TokenManager::HASH];
|
||||||
|
$token = $tokens[TokenManager::TOKEN];
|
||||||
|
$timestamp = $tokens[TokenManager::TIMESTAMP];
|
||||||
|
|
||||||
|
$verification = $tokenManager->verify($hash, $token, $user, $timestamp);
|
||||||
|
|
||||||
|
$this->assertFalse($verification);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user