From f5fd66706bc687b31da32fe65de78d3fe79c3c98 Mon Sep 17 00:00:00 2001 From: "R. Eric Wheeler" Date: Fri, 17 Mar 2017 10:30:04 -0700 Subject: [PATCH] Controller work --- .gitignore | 2 - .php_cs | 9 +- CHANGELOG.md | 2 + app/Kernel.php | 26 +- app/views/form/token.html.twig | 33 +++ app/views/user/index.html.twig | 4 + app/views/user/logout.html.twig | 10 + bin/console | 1 + bin/console.php | 32 +-- composer.json | 2 - html/index.php | 24 +- .../App/Controller/DefaultController.php | 47 ---- .../App/Controller/RouterCollector.php | 43 ++++ .../App/Controller/RsvpController.php | 4 +- .../App/Controller/RsvpControllerProvider.php | 54 ++++ .../App/Controller/UserController.php | 234 ++++++++++++++++++ .../App/Controller/UserControllerProvider.php | 59 +++++ src/Sikofitt/App/Form/UserTokenType.php | 89 +++++++ .../App/Provider/DoctrineConsoleProvider.php | 22 +- .../App/Repository/UserRepository.php | 109 ++++++-- src/Sikofitt/Security/TokenGenerator.php | 60 +++++ 21 files changed, 739 insertions(+), 127 deletions(-) create mode 100644 app/views/form/token.html.twig create mode 100644 app/views/user/index.html.twig create mode 100644 app/views/user/logout.html.twig create mode 120000 bin/console create mode 100644 src/Sikofitt/App/Controller/RouterCollector.php create mode 100644 src/Sikofitt/App/Controller/RsvpControllerProvider.php create mode 100644 src/Sikofitt/App/Controller/UserController.php create mode 100644 src/Sikofitt/App/Controller/UserControllerProvider.php create mode 100644 src/Sikofitt/App/Form/UserTokenType.php create mode 100644 src/Sikofitt/Security/TokenGenerator.php diff --git a/.gitignore b/.gitignore index 683c9d3..73fe2a3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,4 @@ html/css/ html/images/ html/js/ lib/ -t.php -test.php variables.less diff --git a/.php_cs b/.php_cs index 9afb8c5..68b3b2e 100644 --- a/.php_cs +++ b/.php_cs @@ -52,6 +52,11 @@ return PhpCsFixer\Config::create() ->ignoreDotFiles(true) ->ignoreVCS(true) ->name('*.php') - ->in('src') - ->in('html') + ->name(__DIR__.'/app/Kernel.php') + ->in([ + 'src', + 'app', + 'bin', + 'html', + ]) ); \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ebbf62f..d030b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - .php_cs - php-cs-fixer +- Console +- Doctrine/DoctrineProvider/OrmProvider/DoctrineConsoleProvider ## [0.0.1] - 2017-02-07 ### Added diff --git a/app/Kernel.php b/app/Kernel.php index 42bcb9d..819eeef 100644 --- a/app/Kernel.php +++ b/app/Kernel.php @@ -30,7 +30,6 @@ use Monolog\{ Handler\StreamHandler, Logger }; -use Sikofitt\App\Controller\DefaultController; use Sikofitt\App\Entity\User; use Sikofitt\App\Traits\EntityManagerTrait; use Sikofitt\App\Traits\FlashTrait; @@ -86,7 +85,6 @@ class Kernel extends Application use TwigTrait; use UrlGeneratorTrait; - /** * Kernel constructor. * @@ -108,12 +106,11 @@ class Kernel extends Application $this->setUpLogger(); $this->setUpMailer(); } + public function setUpRoutes(\Kernel $app) { - $app->match('/login', DefaultController::class.'::loginAction') - ->method('GET|POST'); - } + /** * @param array $values * @@ -228,10 +225,16 @@ class Kernel extends Application 'password' => 'doughnut', ], ]); - + //$j = new Doctrine\Common\Cache\FilesystemCache() $this->register(new DoctrineOrmServiceProvider(), [ - 'orm.proxies_dir' => $this->getCacheDir().'/doctrine/proxies', - 'orm.default_cache' => 'array', + 'orm.proxies_dir' => $this->getCacheDir().'/doctrine/proxies/Doughnut/Wedding', + 'orm.proxies_namespace' => 'Doughnut\Wedding', + 'orm.default_cache' => [ + 'driver' => 'filesystem', + 'path' => $this->getCacheDir().'/doctrine/cache', + ], + 'orm.auto_generate_proxies' => true, + 'orm.strategy' => 'naming', 'orm.em.options' => [ 'connection' => 'default', 'mappings' => [ @@ -239,6 +242,8 @@ class Kernel extends Application 'type' => 'annotation', 'path' => $this->getBaseDir().'/src/Sikofitt/App/Entity', 'namespace' => 'Sikofitt\App\Entity', + 'alias' => 'Sikofitt', + 'auto_generate_proxies' => true, 'use_simple_annotation_reader' => false, ], ], @@ -327,10 +332,10 @@ class Kernel extends Application // 'anonymous' => true );*/ - $this['protected_pages'] = function() { + $this['protected_pages'] = function () { return [ 'gallery', - 'rsvp/update' + 'rsvp/update', ]; }; @@ -341,7 +346,6 @@ class Kernel extends Application //->register(new SecurityServiceProvider()) ; - $this->extend('form.extensions', function ($extensions) { return $extensions; }); diff --git a/app/views/form/token.html.twig b/app/views/form/token.html.twig new file mode 100644 index 0000000..bc359f2 --- /dev/null +++ b/app/views/form/token.html.twig @@ -0,0 +1,33 @@ +{% extends 'base.html.twig' %} +{% block body %} + {{ dump(app.session.get('user')) }} + {{ form_start(form) }} +
+ Enter Token +
+
+
+ {{ form_row(form.user_token) }} +
+
+ {{ form_row(form.submit) }} +
+
+
+
+

+ You can also send a new auto login link to your email. +

+
+
+ {{ form_row(form.email) }} +
+
+ {{ form_row(form.update_token) }} +
+
+
+ {{ form_rest(form) }} +
+ {{ form_end(form) }} +{% endblock %} \ No newline at end of file diff --git a/app/views/user/index.html.twig b/app/views/user/index.html.twig new file mode 100644 index 0000000..a8e1fc6 --- /dev/null +++ b/app/views/user/index.html.twig @@ -0,0 +1,4 @@ +{% extends 'base.html.twig' %} +{% block body %} +{{ dump(app.session.get('user')) }} +{% endblock %} diff --git a/app/views/user/logout.html.twig b/app/views/user/logout.html.twig new file mode 100644 index 0000000..b25ff8b --- /dev/null +++ b/app/views/user/logout.html.twig @@ -0,0 +1,10 @@ +{% extends 'base.html.twig' %} +{% block body %} +

You have been successfully logged out.

+

+ You can log in again by going to the login page. +

+

+ You can also login using a token by going to the token login page. +

+{% endblock %} \ No newline at end of file diff --git a/bin/console b/bin/console new file mode 120000 index 0000000..cf4512a --- /dev/null +++ b/bin/console @@ -0,0 +1 @@ +console.php \ No newline at end of file diff --git a/bin/console.php b/bin/console.php index 9de444c..1c15ab4 100755 --- a/bin/console.php +++ b/bin/console.php @@ -19,14 +19,10 @@ * along with this program. If not, see . */ -use Doctrine\ORM\Tools\Console\Command\ClearCache\EntityRegionCommand; -use Doctrine\ORM\Tools\Console\Command\MappingDescribeCommand; use Knp\Provider\ConsoleServiceProvider; -use PhpCsFixer\Console\Command\DescribeCommand; -use PhpCsFixer\Console\Command\SelfUpdateCommand; use Sikofitt\App\Provider\DoctrineConsoleProvider; -use Symfony\Component\Console\Application; -use Symfony\Component\Yaml\Command\LintCommand; +use Symfony\Bridge\Twig\Command\DebugCommand; +use Symfony\Bridge\Twig\Command\LintCommand; $loader = require __DIR__.'/../vendor/autoload.php'; $app = new \Kernel($loader, true); @@ -39,12 +35,20 @@ $consoleConfig = [ $app ->register(new ConsoleServiceProvider(), $consoleConfig) ->register(new DoctrineConsoleProvider()); -/** - * @var Application $console - */ -$console = $app['console']; -$console->add(new Symfony\Bridge\Twig\Command\LintCommand()); -$console->add(new Symfony\Bridge\Twig\Command\DebugCommand()); -$console->add(new PhpCsFixer\Console\Command\FixCommand()); -$console->add(new Symfony\Component\Yaml\Command\LintCommand()); + +$fixCommand = new \PhpCsFixer\Console\Command\FixCommand(); +$fixCommand->setName('dev:fixer')->setDescription('PhpCSFixer - Fixes directories and files according to a set of rules.'); +$twigLintCommand = new LintCommand(); +$twigLintCommand->setTwigEnvironment($app['twig']); +$twigDebugCommand = new DebugCommand(); +$twigDebugCommand->setTwigEnvironment($app['twig']); +$fixCommand->setHidden(true); + +$app['console']->addCommands([ + $twigDebugCommand, + $twigLintCommand, + new \Symfony\Component\Yaml\Command\LintCommand(), + $fixCommand, +]); + $app['console']->run(); diff --git a/composer.json b/composer.json index 75f11e0..c2cd2cd 100644 --- a/composer.json +++ b/composer.json @@ -20,8 +20,6 @@ "paragonie/cookie": "^3.1", "paragonie/csp-builder": "^2.0", "paragonie/sodium_compat": "^0.4.0", - "psr/http-message": "^1.0", - "psr/log": "^1.0", "silex/silex": "^2.0", "swiftmailer/swiftmailer": "^5.4", "symfony/asset": "^3.2", diff --git a/html/index.php b/html/index.php index fefff3c..953bd13 100644 --- a/html/index.php +++ b/html/index.php @@ -18,8 +18,8 @@ * along with this program. If not, see . */ -use Sikofitt\App\Controller\DefaultController; -use Sikofitt\App\Controller\RsvpController; +use Sikofitt\App\Controller\RsvpControllerProvider; +use Sikofitt\App\Controller\UserController; use Sikofitt\App\Middleware\CspMiddleware; use Sikofitt\App\Middleware\HeaderMiddleware; @@ -28,25 +28,11 @@ $loader = require __DIR__.'/../vendor/autoload.php'; $app = new Kernel($loader, true); // Controllers // Default -$app->setUpRoutes($app); -//$app->match('/login', DefaultController::class.'loginAction') -// ->bind('login'); -// RSVP Actions +$app->mount('/rsvp', new RsvpControllerProvider()); + +$app->mount('/user', new UserController()); -$app->match('/rsvp', RsvpController::class.'::indexAction') - ->method('GET|POST') - ->bind('rsvp'); -$app->match('/rsvp/reset', RsvpController::class.'::resetAction') - ->method('GET|POST') - ->bind('rsvp_password_reset'); -$app->match('/rsvp/reset/{token}', RsvpController::class.'::tokenAction') - ->bind('rsvp_token') - ->method('GET|POST'); - //->before(new MysqlAuthenticatorMiddleware()); -$app->match('/rsvp/edit', RsvpController::class.'::editAction') - ->method('GET|POST') - ->bind('rsvp_edit'); //->before(new MysqlAuthenticatorMiddleware()); // Middleware $app->before(new CspMiddleware(), \Kernel::EARLY_EVENT); diff --git a/src/Sikofitt/App/Controller/DefaultController.php b/src/Sikofitt/App/Controller/DefaultController.php index 716e536..37daf37 100644 --- a/src/Sikofitt/App/Controller/DefaultController.php +++ b/src/Sikofitt/App/Controller/DefaultController.php @@ -20,9 +20,6 @@ namespace Sikofitt\App\Controller; -use Sikofitt\App\Entity\User; -use Sikofitt\App\Form\UserLoginType; -use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; class DefaultController @@ -31,48 +28,4 @@ class DefaultController { return $app->render('index.html.twig', ['request' => $request]); } - - public function loginAction(Request $request, \Kernel $app) - { - if ($app->session()->has('user')) { - //return $app->redirect($app->url('rsvp_edit')); - } - $loginForm = $app->getFormFactory()->create(UserLoginType::class); - if ($request->isMethod('POST')) { - $loginForm->handleRequest($request); - if ($loginForm->isValid() && $loginForm->isSubmitted()) { - $user = $app->getEntityManager()->getRepository(User::class)->findByEmail($loginForm->get('email_username')->getData()); - if (null !== $user && true === password_verify($loginForm->get('password')->getData(), $user[0]->getPassword())) { - $userSession = [ - 'firstName' => $user[0]->getFirstName(), - 'lastName' => $user[0]->getLastName(), - 'fullName' => sprintf('%s %s', $user[0]->getFirstName(), $user[0]->getLastName()), - 'familySide' => $user[0]->getFamilySide(), - 'email' => $user[0]->getEmail(), - 'family' => $user[0]->getFamily(), - 'created' => $user[0]->getCreated()->format('U'), - 'updated' => $user[0]->getUpdated()->format('U'), - 'guests' => $user[0]->getRsvp()->getGuests(), - ]; - $app->getSession()->set('user', $userSession); - $app->redirect($app->url('rsvp')); - } else { - $error = new FormError('Your password or email is incorrect.'); - $error->setOrigin($loginForm); - $loginForm->get('password')->addError($error); - - return $app->render('login.html.twig', ['form' => $loginForm->createView()]); - } - } - } - - return $app->render('login.html.twig', ['form' => $loginForm->createView()]); - } - - public function logoutAction(Request $request, \Kernel $app) - { - $app->session()->remove('user'); - - return $app->render('logout.html.twig'); - } } diff --git a/src/Sikofitt/App/Controller/RouterCollector.php b/src/Sikofitt/App/Controller/RouterCollector.php new file mode 100644 index 0000000..2c53fd6 --- /dev/null +++ b/src/Sikofitt/App/Controller/RouterCollector.php @@ -0,0 +1,43 @@ +. + */ + +namespace Sikofitt\App\Controller; + +class RouterCollector +{ + /** + * @var \Kernel + */ + private $application; + + public function setApplication(\Kernel $application) + { + $this->application = $application; + + return $this; + } + + public function buildRoutes() + { + $this->application->match('/login', DefaultController::class.'::loginAction') + ->method('GET|POST') + ->bind('login'); + } +} diff --git a/src/Sikofitt/App/Controller/RsvpController.php b/src/Sikofitt/App/Controller/RsvpController.php index 9f196f9..ff7112e 100644 --- a/src/Sikofitt/App/Controller/RsvpController.php +++ b/src/Sikofitt/App/Controller/RsvpController.php @@ -45,9 +45,9 @@ class RsvpController * @var UserRepository $userRepo */ $em = $app['orm.em']; - $rsvpRepo = $em->getRepository('Sikofitt\App\Entity\Rsvp'); + $rsvpRepo = $em->getRepository('Sikofitt:Rsvp'); $count = (40 - $rsvpRepo->getRsvpCount()); - $userRepo = $em->getRepository('Sikofitt\App\Entity\User'); + $userRepo = $em->getRepository('Sikofitt:User'); $kCount = $userRepo->getKatrinaCount(); $eCount = $userRepo->getEricCount(); /** diff --git a/src/Sikofitt/App/Controller/RsvpControllerProvider.php b/src/Sikofitt/App/Controller/RsvpControllerProvider.php new file mode 100644 index 0000000..266f59a --- /dev/null +++ b/src/Sikofitt/App/Controller/RsvpControllerProvider.php @@ -0,0 +1,54 @@ +. + */ + +namespace Sikofitt\App\Controller; + +use Pimple\Container; +use Silex\Api\ControllerProviderInterface; +use Silex\Application; +use Silex\ControllerCollection; + +class RsvpControllerProvider implements ControllerProviderInterface +{ + /** + * Returns routes to connect to the given application. + * + * @param Application $app An Application instance + * + * @return ControllerCollection A ControllerCollection instance + */ + public function connect(Application $app) + { + /** + * @var ControllerCollection $rsvpControllers; + */ + $rsvpControllers = $app['controllers_factory']; + $rsvpControllers + ->match('/', RsvpController::class.'::indexAction') + ->method('GET|POST') + ->bind('rsvp'); + + return $rsvpControllers; + } + + public function register(Container $pimple) + { + } +} diff --git a/src/Sikofitt/App/Controller/UserController.php b/src/Sikofitt/App/Controller/UserController.php new file mode 100644 index 0000000..7199d5d --- /dev/null +++ b/src/Sikofitt/App/Controller/UserController.php @@ -0,0 +1,234 @@ +. + */ + +namespace Sikofitt\App\Controller; + +use Sikofitt\App\Form\UserLoginType; +use Sikofitt\App\Form\UserTokenType; +use Sikofitt\Security\TokenGenerator; +use Silex\Api\ControllerProviderInterface; +use Silex\Application; +use Symfony\Component\Form\FormError; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * Class UserController. + */ +class UserController implements ControllerProviderInterface +{ + public function connect(Application $app) + { + /** + * @var ControllerCollection $userControllers + */ + $userControllers = $app['controllers_factory']; + $userControllers->match('/', __CLASS__.'::indexAction') + ->method('GET') + ->bind('user_index'); + + $userControllers->match('/login', __CLASS__.'::loginAction') + ->method('GET|POST') + ->bind('login'); + $userControllers->get('/logout', __CLASS__.'::logoutAction') + ->bind('logout'); + $userControllers->match('/login/token/{token}', __CLASS__.'::tokenLoginAction') + ->method('GET|POST') + ->bind('token_login') + ->value('token', null); + + $userControllers->match('/reset', __CLASS__.'::resetAction') + ->method('GET|POST') + ->bind('user_reset'); + $userControllers->match('/reset/{token}', __CLASS__.'::tokenAction') + ->bind('user_reset_token') + ->method('GET|POST'); + //->before(new MysqlAuthenticatorMiddleware()); + $userControllers->match('/edit', __CLASS__.'::editAction') + ->method('GET|POST') + ->bind('user_edit'); + + return $userControllers; + } + + public function logoutAction(\Kernel $app) + { + if (true === $app->session()->has('user')) { + $app->session()->remove('user'); + } + + return $app->render('user/logout.html.twig'); + } + + public function tokenLoginAction(Request $request, \Kernel $app, string $token = null) + { + if (null === $token) { + $tokenForm = $app->getFormFactory()->create(UserTokenType::class); + if ($request->isMethod('POST')) { + $tokenForm->handleRequest($request); + if ($tokenForm->isValid() && $tokenForm->isSubmitted()) { + if ($tokenForm->get('update_token')->isClicked()) { + if (null !== $tokenForm->get('email')->getData()) { + return $this->updateAndSendTokenLoginLink($app, + $tokenForm); + } else { + $tokenForm->get('email') + ->addError(new FormError('Email address is a required field to send a new login link.')); + + return $app->render('form/token.html.twig', + ['form' => $tokenForm->createView()]); + } + } else { + $userToken = $tokenForm->get('user_token')->getData(); + $user = $app->getEntityManager() + ->getRepository('Sikofitt:User') + ->getUserByUserToken($userToken); + if (null === $user) { + $tokenForm->get('user_token') + ->addError(new FormError('Token is invalid.')); + } else { + $app->session()->set('user', $user); + } + + return $app->render('form/token.html.twig', + ['form' => $tokenForm->createView()]); + } + } else { + return $app->render('form/token.html.twig', + ['form' => $tokenForm->createView()]); + } + } + + return $app->render('form/token.html.twig', ['form' => $tokenForm->createView()]); + } else { + // Token has been included. + $app->session()->remove('user'); + $tokenForm = $app->getFormFactory()->create(UserTokenType::class); + //$user = $app->getEntityManager()->getRepository('Sikofitt:User')->getUserByUserToken($token); + $user = $app->getEntityManager()->getRepository('Sikofitt:User')->findOneBy(['userToken' => $token]); + if (null !== $user) { + $app->session()->set('user', $user); + + return $app->render('user/index.html.twig'); + } else { + return new StreamedResponse(function () use ($app, $tokenForm, $token) { + $tokenForm->get('user_token')->setData($token); + $tokenForm->get('user_token')->addError(new FormError('Invalid token.')); + print $app->renderView('form/token.html.twig', ['form' => $tokenForm->createView()]); + }); + } + } + } + + public function indexAction(Request $request, \Kernel $app) + { + if ($app->session()->has('user')) { + return new JsonResponse( + [ + 'request' => $request->request->all(), + 'server' => $request->server->all(), + 'headers' => $request->headers->all(), + 'session' => $app->getSession()->get('user'), + 'token' => (string) new TokenGenerator(), + ] + ); + } + } + + public function loginAction(Request $request, \Kernel $app) + { + if ($app->session()->has('user')) { + //return $app->redirect($app->url('rsvp_edit')); + } + $loginForm = $app->getFormFactory()->create(UserLoginType::class); + if ($request->isMethod('POST')) { + $loginForm->handleRequest($request); + if ($loginForm->isValid() && $loginForm->isSubmitted()) { + $user = $app->getEntityManager()->getRepository(User::class)->findByEmail($loginForm->get('email_username')->getData()); + if (null !== $user && true === password_verify($loginForm->get('password')->getData(), $user[0]->getPassword())) { + $userSession = [ + 'firstName' => $user[0]->getFirstName(), + 'lastName' => $user[0]->getLastName(), + 'fullName' => sprintf('%s %s', $user[0]->getFirstName(), $user[0]->getLastName()), + 'familySide' => $user[0]->getFamilySide(), + 'email' => $user[0]->getEmail(), + 'family' => $user[0]->getFamily(), + 'created' => $user[0]->getCreated()->format('U'), + 'updated' => $user[0]->getUpdated()->format('U'), + 'guests' => $user[0]->getRsvp()->getGuests(), + ]; + $app->getSession()->set('user', $userSession); + $app->redirect($app->url('rsvp')); + } else { + $error = new FormError('Your password or email is incorrect.'); + $error->setOrigin($loginForm); + $loginForm->get('password')->addError($error); + + return $app->render('login.html.twig', ['form' => $loginForm->createView()]); + } + } + } + + return $app->render('login.html.twig', ['form' => $loginForm->createView()]); + } + + public function resetAction(Request $request, \Kernel $app) + { + } + + public function tokenAction(Request $request, \Kernel $app) + { + } + + public function editAction(Request $request, \Kernel $app) + { + } + + private function tokenFormGenerator(\Kernel $app) + { + return; + } + + private function updateAndSendTokenLoginLink(\Kernel $app, \Symfony\Component\Form\FormInterface $tokenForm) + { + $email = $tokenForm->get('email')->getData(); + $user = $app->getEntityManager() + ->getRepository('Sikofitt:User') + ->findByEmail($email); + if (null === $user) { + $tokenForm->get('email')->addError(new FormError('Sorry we couldn\'t find your email address.')); + + return $app->render('form/token.html.twig', ['form' => $tokenForm->createView()]); + } + $newToken = $app->getEntityManager() + ->getRepository('Sikofitt:User') + ->setUserToken($email); + if (false !== $newToken) { + $user->setUserToken($newToken); + $app->session()->set('user', $user); + + return $app->render('form/token.html.twig', ['form' => $tokenForm->createView()]); + } + $tokenForm->get('email')->addError(new FormError('An Unknown error occured. Please try again.')); + + return $app->render('form/token.html.twig', ['form' => $tokenForm->createView()]); + } +} diff --git a/src/Sikofitt/App/Controller/UserControllerProvider.php b/src/Sikofitt/App/Controller/UserControllerProvider.php new file mode 100644 index 0000000..86436ed --- /dev/null +++ b/src/Sikofitt/App/Controller/UserControllerProvider.php @@ -0,0 +1,59 @@ +. + */ + +namespace Sikofitt\App\Controller; + +use Silex\Api\ControllerProviderInterface; +use Silex\Application; +use Silex\ControllerCollection; + +class UserControllerProvider implements ControllerProviderInterface +{ + /** + * Returns routes to connect to the given application. + * + * @param Application $app An Application instance + * + * @return ControllerCollection A ControllerCollection instance + */ + public function connect(Application $app) + { + /** + * @var ControllerCollection $userControllers + */ + $userControllers = $app['controllers_factory']; + $app->match('/user', UserController::class.'::indexAction') + ->method('GET') + ->bind('user_index'); + + $app->match('/user/reset', UserController::class.'::resetAction') + ->method('GET|POST') + ->bind('user_reset'); + $app->match('/user/reset/{token}', UserController::class.'::tokenAction') + ->bind('user_reset_token') + ->method('GET|POST'); + //->before(new MysqlAuthenticatorMiddleware()); + $app->match('/user/edit', UserController::class.'::editAction') + ->method('GET|POST') + ->bind('user_edit'); + + return $userControllers; + } +} diff --git a/src/Sikofitt/App/Form/UserTokenType.php b/src/Sikofitt/App/Form/UserTokenType.php new file mode 100644 index 0000000..42ec935 --- /dev/null +++ b/src/Sikofitt/App/Form/UserTokenType.php @@ -0,0 +1,89 @@ +. + */ + +namespace Sikofitt\App\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\Length; + +class UserTokenType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('user_token', TextType::class, [ + 'attr' => [ + 'class' => 'uk-input uk-form-large', + 'placehoder' => 'Token', + ], + 'required' => false, + 'label_attr' => [ + 'class' => 'uk-form-label uk-hidden', + ], + 'constraints' => [ + new Length([ + 'min' => 64, + 'max' => 64, + 'minMessage' => 'Token should be exactly {{ limit }} characters.', + 'maxMessage' => 'Token should be exactly {{ limit }} characters.', + 'exactMessage' => 'Token should be exactly {{ limit }} characters.', + ]), + ], + ])->add('submit', SubmitType::class, [ + 'attr' => [ + 'class' => 'uk-button uk-button-large uk-button-primary', + ], + 'label' => 'Login', + ])->add('email', EmailType::class, [ + 'required' => false, + 'constraints' => [ + new Email([ + 'strict' => true, + 'checkMX' => true, + 'checkHost' => true, + ]), + ], + 'attr' => [ + 'class' => 'uk-input uk-form-large uk-text-center', + 'placeholder' => 'Email Address', + ], + 'label_attr' => [ + 'class' => 'uk-form-label', + ], + ])->add('update_token', SubmitType::class, [ + 'attr' => [ + 'class' => 'uk-button uk-button-large uk-button-primary', + ], + 'label' => 'Send New Login Link', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('attr', ['class' => 'uk-form uk-align-center']); + } +} diff --git a/src/Sikofitt/App/Provider/DoctrineConsoleProvider.php b/src/Sikofitt/App/Provider/DoctrineConsoleProvider.php index 43a55db..49d2303 100644 --- a/src/Sikofitt/App/Provider/DoctrineConsoleProvider.php +++ b/src/Sikofitt/App/Provider/DoctrineConsoleProvider.php @@ -20,12 +20,12 @@ namespace Sikofitt\App\Provider; - use Doctrine\DBAL\Tools\Console\Command\{ ImportCommand, ReservedWordsCommand, RunSqlCommand }; +use Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper; use Doctrine\ORM\Tools\Console\Command\{ ClearCache\EntityRegionCommand, ClearCache\MetadataCommand, ClearCache\QueryCommand, ClearCache\ResultCommand, @@ -52,6 +52,8 @@ class DoctrineConsoleProvider implements ServiceProviderInterface * It should not get services. * * @param Container $pimple A container instance + * + * @throws \LogicException */ public function register(Container $pimple) { @@ -61,24 +63,26 @@ class DoctrineConsoleProvider implements ServiceProviderInterface if (false === isset($pimple['db.options'])) { throw new \LogicException('You must enable the DoctrineServiceProvider to use the DoctrineConsoleProvider.'); } - if(false === isset($pimple['orm.em'])) { + if (false === isset($pimple['orm.em'])) { throw new \LogicException('You must enable the Dflydev\Provider\DoctrineOrm\DoctrineOrmServiceProvider to use the DoctrineConsoleProvider.'); } $console = $pimple['console']; - $console->setHelperSet(new HelperSet(array( - 'em' => new EntityManagerHelper($pimple['orm.em']) - ))); + $console->setHelperSet(new HelperSet([ + 'em' => new EntityManagerHelper($pimple['orm.em']), + 'db' => new ConnectionHelper($pimple['db']), + ])); - $updateCommand = new UpdateCommand(); - $updateCommand->setName('orm:schema:update'); + $schemaUpdateCommand = (new UpdateCommand()) + ->setName('orm:schema:update'); $schemaValidateCommand = (new ValidateSchemaCommand()) ->setName('orm:schema:validate') - ->setAliases(['orm:validate']); + ->setAliases(['validate']); $schemaDropCommand = (new DropCommand()) ->setName('orm:schema:drop'); $schemaCreateCommand = (new CreateCommand()) ->setName('orm:schema:create'); + $queryCommand = new QueryCommand(); $console->addCommands([ new ConvertDoctrine1SchemaCommand(), @@ -99,8 +103,8 @@ class DoctrineConsoleProvider implements ServiceProviderInterface new ResultCommand(), $schemaCreateCommand, $schemaDropCommand, + $schemaUpdateCommand, $schemaValidateCommand, ]); } - } diff --git a/src/Sikofitt/App/Repository/UserRepository.php b/src/Sikofitt/App/Repository/UserRepository.php index 66375bd..9a62f33 100644 --- a/src/Sikofitt/App/Repository/UserRepository.php +++ b/src/Sikofitt/App/Repository/UserRepository.php @@ -23,7 +23,7 @@ namespace Sikofitt\App\Repository; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query; use Sikofitt\App\Entity\User; -use Symfony\Component\Security\Core\User\UserInterface; +use Sikofitt\Security\TokenGenerator; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Validation; @@ -34,17 +34,25 @@ use Symfony\Component\Validator\Validation; */ class UserRepository extends EntityRepository { + /** + * @param string $email + * + * @return null|User + */ public function findByEmail(string $email) { - return $this->findBy(['email' => $email]); - /*return $this->createQueryBuilder('u') - ->select('u') + return $this->createQueryBuilder('u') + ->select(['u', 'r']) + ->leftJoin('u.rsvp', 'r', 'WITH', 'u.rsvp = r.id') ->where('u.email = :email') ->setParameter('email', $email) ->getQuery() - ->getOneOrNullResult(Query::HYDRATE_OBJECT);*/ + ->getOneOrNullResult(Query::HYDRATE_OBJECT); } + /** + * @return int + */ public function getKatrinaCount() { return $this->createQueryBuilder('u') @@ -53,9 +61,12 @@ class UserRepository extends EntityRepository ->where('u.familySide = :side') ->setParameter('side', User::KATRINA_SIDE) ->getQuery() - ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR) ?? 0; } + /** + * @return int + */ public function getEricCount() { return $this->createQueryBuilder('u') @@ -64,9 +75,14 @@ class UserRepository extends EntityRepository ->where('u.familySide = :side') ->setParameter('side', User::ERIC_SIDE) ->getQuery() - ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR) ?? 0; } + /** + * @param string $email + * + * @return mixed|\Symfony\Component\Validator\ConstraintViolationListInterface + */ public function getEmail(string $email) { $validator = Validation::createValidator(); @@ -90,35 +106,90 @@ class UserRepository extends EntityRepository ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); } - public function getUserByToken(string $token) + /** + * @param string $resetToken + * + * @return User|null + */ + public function getUserByResetToken(string $resetToken) { return $this->createQueryBuilder('u') - ->select(['u']) - ->where('u.token = :token') - ->setParameter('token', $token) + ->select(['u', 'r']) + ->leftJoin('u.rsvp', 'r', 'WITH', 'u.rsvp = r.id') + ->where('u.resetToken = :resetToken') + ->setParameter('resetToken', $resetToken) ->getQuery() - ->getOneOrNullResult(); + ->getOneOrNullResult(Query::HYDRATE_OBJECT); } + /** + * @param string $userToken + * + * @return User|null + */ + public function getUserByUserToken(string $userToken) + { + return $this->createQueryBuilder('u') + ->select(['u', 'r']) + ->leftJoin('u.rsvp', 'r', 'WITH', 'u.rsvp = r.id') + ->where('u.userToken = :userToken') + ->setParameter('userToken', $userToken) + ->getQuery() + ->getOneOrNullResult(Query::HYDRATE_OBJECT); + } + + /** + * Sets or updates user's user login token. + * + * @param string $email + * + * @return bool|string + */ + public function setUserToken(string $email) + { + $userToken = (string) new TokenGenerator(); + + $result = (bool) $this->createQueryBuilder('u') + ->update() + ->set('u.userToken', ':userToken') + ->setParameter('userToken', $userToken) + ->where('u.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + + return true === $result ? $userToken : false; + } + + /** + * Sets or updates user's reset password token. + * + * @param string $email + * + * @return bool + */ public function setResetToken(string $email) { - $bytes = \ParagonIE_Sodium_Compat::randombytes_buf(22); - $blake = \ParagonIE_Sodium_Compat::crypto_generichash($bytes); - $token = \ParagonIE_Sodium_Core_BLAKE2b::bin2hex($blake); + $resetToken = (string) new TokenGenerator(); return (bool) $this->createQueryBuilder('u') ->update() - ->set('u.token', ':token') - ->setParameter('token', $token) + ->set('u.resetToken', ':resetToken') + ->setParameter('resetToken', $resetToken) ->where('u.email = :email') ->setParameter('email', $email) ->getQuery() ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); } - public function updatePassword(UserInterface $user) + /** + * Updates a user. + * + * @param User|\Symfony\Component\Security\Core\User\UserInterface $user + */ + public function destoryUserResetToken(User $user) { - $user->setToken(null); + $user->setResetToken(null); $this->getEntityManager()->persist($user); $this->getEntityManager()->flush(); } diff --git a/src/Sikofitt/Security/TokenGenerator.php b/src/Sikofitt/Security/TokenGenerator.php new file mode 100644 index 0000000..5a63ccc --- /dev/null +++ b/src/Sikofitt/Security/TokenGenerator.php @@ -0,0 +1,60 @@ +. + */ + +namespace Sikofitt\Security; + +use ParagonIE_Sodium_Compat as SodiumCompat; +use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface; + +/** + * Class TokenGenerator. + */ +class TokenGenerator implements TokenGeneratorInterface +{ + public function __toString() + { + return $this->generateToken(); + } + + /** + * Generates a reset or user token. + * + * @return string The generated token + */ + public function generateToken() + { + $randomBytes = substr( + SodiumCompat::randombytes_buf(512), + random_int(SodiumCompat::CRYPTO_GENERICHASH_BYTES, 512), + SodiumCompat::CRYPTO_GENERICHASH_BYTES_MAX + ); + + $rawToken = SodiumCompat::crypto_generichash( + $randomBytes, + SodiumCompat::randombytes_buf( + SodiumCompat::CRYPTO_GENERICHASH_BYTES_MAX + ), + 32 + ); + $token = SodiumCompat::bin2hex($rawToken); + + return $token; + } +}