From e46f6e08109b78740fc335922de93663970b9cd5 Mon Sep 17 00:00:00 2001 From: "R. Eric Wheeler" Date: Fri, 10 Feb 2017 21:34:31 -0800 Subject: [PATCH] WIP: Saving Local Branch --- .gitignore | 13 +- .php_cs | 2 +- Gruntfile.js | 142 +++++++ app/Kernel.php | 354 ++++++++++++++++ app/config/csp.json | 49 +++ app/config/database.yml | 20 + app/views/base.html.twig | 20 + app/views/flash_messages.html.twig | 20 + app/views/index.html.twig | 8 + app/views/reset_password.html.twig | 21 + app/views/reset_password_confirm.html.twig | 5 + app/views/rsvp_form.html.twig | 70 ++++ build/js/doughnut.js | 3 + build/less/doughnut.less | 21 + .../templates/index.html | 0 cli-config.php | 21 + composer.json | 64 ++- docker-compose.yml | 4 + docker/php/Dockerfile | 2 + doctrine.php | 30 ++ html/index.php | 36 +- package.json | 6 + .../Configuration/DatabaseConfiguration.php | 96 +++++ .../App/Controller/DefaultController.php | 75 ++++ .../App/Controller/RsvpController.php | 160 +++++++ src/Sikofitt/App/Entity/Rsvp.php | 183 ++++++++ src/Sikofitt/App/Entity/User.php | 390 ++++++++++++++++++ src/Sikofitt/App/Form/ResetType.php | 78 ++++ src/Sikofitt/App/Form/RsvpType.php | 141 +++++++ src/Sikofitt/App/Middleware/CspMiddleware.php | 56 +++ .../App/Middleware/HeaderMiddleware.php | 44 ++ .../App/Repository/RsvpRepository.php | 34 ++ .../App/Repository/UserRepository.php | 94 +++++ src/Sikofitt/App/Traits/FlashTrait.php | 96 +++++ src/Sikofitt/Security/MysqlAuthenticator.php | 198 +++++++++ src/Sikofitt/Security/ScryptEncoder.php | 96 +++++ 36 files changed, 2631 insertions(+), 21 deletions(-) create mode 100644 Gruntfile.js create mode 100644 app/Kernel.php create mode 100644 app/config/csp.json create mode 100644 app/config/database.yml create mode 100644 app/views/base.html.twig create mode 100644 app/views/flash_messages.html.twig create mode 100644 app/views/index.html.twig create mode 100644 app/views/reset_password.html.twig create mode 100644 app/views/reset_password_confirm.html.twig create mode 100644 app/views/rsvp_form.html.twig create mode 100644 build/less/doughnut.less rename src/Sikofitt/Http/Kernel/HttpKernel.php => build/templates/index.html (100%) create mode 100644 cli-config.php create mode 100644 doctrine.php create mode 100644 src/Sikofitt/App/Configuration/DatabaseConfiguration.php create mode 100644 src/Sikofitt/App/Controller/DefaultController.php create mode 100644 src/Sikofitt/App/Controller/RsvpController.php create mode 100644 src/Sikofitt/App/Entity/Rsvp.php create mode 100644 src/Sikofitt/App/Entity/User.php create mode 100644 src/Sikofitt/App/Form/ResetType.php create mode 100644 src/Sikofitt/App/Form/RsvpType.php create mode 100644 src/Sikofitt/App/Middleware/CspMiddleware.php create mode 100644 src/Sikofitt/App/Middleware/HeaderMiddleware.php create mode 100644 src/Sikofitt/App/Repository/RsvpRepository.php create mode 100644 src/Sikofitt/App/Repository/UserRepository.php create mode 100644 src/Sikofitt/App/Traits/FlashTrait.php create mode 100644 src/Sikofitt/Security/MysqlAuthenticator.php create mode 100644 src/Sikofitt/Security/ScryptEncoder.php diff --git a/.gitignore b/.gitignore index b03199a..683c9d3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,15 @@ vendor/ *~ lib/mysql/ composer.lock -.idea/ \ No newline at end of file +.idea/ +.php_cs.cache +app/logs/ +build/dist/ +cache/ +html/css/ +html/images/ +html/js/ +lib/ +t.php +test.php +variables.less diff --git a/.php_cs b/.php_cs index 5285396..9afb8c5 100644 --- a/.php_cs +++ b/.php_cs @@ -33,9 +33,9 @@ return PhpCsFixer\Config::create() 'single_import_per_statement' => false, 'phpdoc_order' => true, 'array_syntax' => ['syntax' => 'short'], - 'short_echo_tag' => false, 'phpdoc_add_missing_param_annotation' => true, 'psr4' => true, + 'phpdoc_var_without_name' => false, 'no_extra_consecutive_blank_lines' => [ 'break', 'continue', diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..25329d6 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,142 @@ + +module.exports = function (grunt) { + // Project configuration. + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + uglify: { + options: { + banner: '/*! <%= pkg.name %> javascript <%= grunt.template.today("yyyy-mm-dd") %> */\n', + mangle:false + }, + build: { + src: 'build/dist/js/<%= pkg.name %>.js', + dest: 'build/dist/js/<%= pkg.name %>.min.js' + }, + vendor: { + src: [ + './vendor/bower/jquery/dist/jquery.js', + './vendor/bower/uikit/dist/js/uikit.js' + ], + dest: 'build/dist/js/vendor.min.js' + } + }, + less: { + production: { + options: { + compress: false, + syncImport: true, + plugins: [ + new (require('less-plugin-autoprefix'))({browsers: ["last 2 versions"]}) + ], + paths: [ + 'vendor/bower/uikit/src/less', + 'vendor/bower/uikit/src/less/theme', + 'vendor/bower/uikit/src/less/components' + ] + }, + files: { + 'build/dist/css/<%= pkg.name %>.css': [ + './build/less/doughnut.less', + ] + } + } + }, + cssmin: { + production: { + files: [{ + expand: true, + cwd: 'build/dist/css', + src: ['*.css', '!*.min.css'], + dest: 'build/dist/css', + ext: '.min.css' + }] + } + }, + jshint: { + dev: ['Gruntfile.js', 'build/js/doughnut.js'], + options: { + // options here to override JSHint defaults + reporter: require('jshint-stylish'), + globals: { + jQuery: true, + console: true, + module: true, + document: true + } + } + }, + concat: { + dist: { + src: [ + 'build/js/doughnut.js' + ], + dest: 'build/dist/js/<%= pkg.name %>.js' + } + }, + copy: { + main: { + files: [ + { + expand: true, + cwd: 'build/dist/js', + src: ['*.min.js'], + dest: 'html/js', + filter: 'isFile' + }, + { + expand: true, + cwd: 'build/dist/css', + src: ['*.min.css'], + dest: 'html/css', + filter: 'isFile' + }, + { + expand: true, + cwd: './vendor/bower/uikit/dist/images', + src: ['**'], + dest: 'html/images' + } + ] + } + }, + watch: { + configFiles: { + files: ['Gruntfile.js', 'config/*.js'], + options: { + reload: true + } + }, + less: { + files: 'build/less/*.less', + tasks: ['less', 'cssmin', 'copy'] + }, + js: { + files: 'build/js/doughnut.js', + tasks: ['concat:dist', 'jshint', 'uglify:build', 'copy'] + } + }, + clean: [ + 'build/dist', + 'html/js/*.js', + 'html/css/*.css', + 'html/images/*.svg' + ] + }); + + // Load the plugin that provides the "uglify" task. + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-less'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); + // Default task(s). + grunt.registerTask('build', ['clean', 'concat', 'jshint', 'uglify', 'less', 'cssmin']); + grunt.registerTask('install', ['copy']); + grunt.registerTask('default', ['build', 'copy']); + + +}; \ No newline at end of file diff --git a/app/Kernel.php b/app/Kernel.php new file mode 100644 index 0000000..9ad4035 --- /dev/null +++ b/app/Kernel.php @@ -0,0 +1,354 @@ +. + */ + +use Bramus\Monolog\Formatter\{ + ColorSchemes\TrafficLight, + ColoredLineFormatter +}; + +use Composer\Autoload\ClassLoader; +use Dflydev\Provider\DoctrineOrm\DoctrineOrmServiceProvider; +use Doctrine\Common\Annotations\AnnotationRegistry; +use Monolog\{ + Handler\StreamHandler, + Logger +}; +use Sikofitt\App\Traits\FlashTrait; +use Silex\Application; +use Silex\Application\{ + FormTrait, + MonologTrait, + SecurityTrait, + SwiftmailerTrait, + TranslationTrait, + TwigTrait, + UrlGeneratorTrait +}; +use Silex\Provider\{ + AssetServiceProvider, + CsrfServiceProvider, + DoctrineServiceProvider, + ExceptionHandlerServiceProvider, + FormServiceProvider, + HttpFragmentServiceProvider, + HttpKernelServiceProvider, + MonologServiceProvider, + RoutingServiceProvider, + SecurityServiceProvider, + ServiceControllerServiceProvider, + SessionServiceProvider, + SwiftmailerServiceProvider, + TwigServiceProvider, + ValidatorServiceProvider, + VarDumperServiceProvider, + WebProfilerServiceProvider +}; + +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\Form\FormFactory; +use Symfony\Component\Translation\Translator; + +/** + * Class Kernel. + */ +class Kernel extends Application +{ + use FlashTrait; + use FormTrait; + use MonologTrait; + use SecurityTrait; + use SwiftmailerTrait; + use TranslationTrait; + use TwigTrait; + use UrlGeneratorTrait; + + /** + * Kernel constructor. + * + * @param ClassLoader $loader + * @param bool $debug + * @param array $values + */ + public function __construct(ClassLoader $loader, bool $debug = false, array $values = []) + { + parent::__construct($values); + AnnotationRegistry::registerLoader([$loader, 'loadClass']); + if (true === $debug) { + $this->setDebug(); + } + $this->setUpProviders(); + $this->setUpDatabase(); + $this->setUpView(); + $this->setUpLogger(); + $this->setUpMailer(); + } + + /** + * @param array $values + * + * @throws \InvalidArgumentException + * + * @return int + */ + public function mail(array $values) + { + if (false === isset($values['from']) || false === is_array($values['from'])) { + throw new \InvalidArgumentException('Array key "from" should be an array.'); + } elseif (false === isset($values['to']) || false === is_array($values['to'])) { + throw new \InvalidArgumentException('Array key "to" should be an array.'); + } elseif (false === isset($values['body']) || false === is_string($values['body'])) { + throw new \InvalidArgumentException('Array key "body" should be a string.'); + } elseif (false === isset($values['subject']) || false === is_string($values['subject'])) { + throw new \InvalidArgumentException('Array key "subject" should be a string.'); + } + + $message = \Swift_Message::newInstance(); + $message + ->setSubject($values['subject']) + ->setFrom($values['from']) + ->setTo($values['to']) + ->setBody($values['body']); + + /** + * @var \Swift_Transport $mailer + */ + $mailer = $this['mailer']; + + return $mailer->send($message); + } + + /** + * Sets the application to debug. + */ + public function setDebug() + { + $this['debug'] = true; + ErrorHandler::register(); + ExceptionHandler::register(); + } + + /** + * @return bool + */ + public function getDebug(): bool + { + return $this['debug']; + } + + /** + * @return string + */ + public function getBaseDir(): string + { + return __DIR__.'/..'; + } + + /** + * @return string + */ + public function getAppDir(): string + { + return $this->getBaseDir().'/app'; + } + + /** + * @return string + */ + public function getConfigDir(): string + { + return $this->getAppDir().'/config'; + } + + /** + * @return string + */ + public function getLogDir(): string + { + return $this->getAppDir().'/logs'; + } + + /** + * @return string + */ + public function getCacheDir(): string + { + return $this->getBaseDir().'/cache'; + } + + /** + * @return FormFactory + */ + public function getFormFactory() + { + return $this['form.factory']; + } + + /** + * Sets up the database environment. + */ + protected function setUpDatabase() + { + $this->register(new DoctrineServiceProvider(), [ + 'db.options' => [ + 'driver' => 'pdo_mysql', + 'dbname' => 'doughnut', + 'host' => 'mysql', + 'user' => 'doughnut', + 'password' => 'doughnut', + ], + ]); + + $this->register(new DoctrineOrmServiceProvider(), [ + 'orm.proxies_dir' => $this->getCacheDir().'/doctrine/proxies', + 'orm.default_cache' => 'array', + 'orm.em.options' => [ + 'connection' => 'default', + 'mappings' => [ + [ + 'type' => 'annotation', + 'path' => $this->getBaseDir().'/src/Sikofitt/App/Entity', + 'namespace' => 'Sikofitt\App\Entity', + 'use_simple_annotation_reader' => false, + ], + ], + ], + ]); + } + + /** + * Sets up the view for the application. + */ + protected function setUpView() + { + $this + ->register(new HttpKernelServiceProvider()) + ->register(new RoutingServiceProvider()) + ->register(new AssetServiceProvider()) + ->register(new SessionServiceProvider()) + ->register(new HttpFragmentServiceProvider()) + ->register(new ServiceControllerServiceProvider()) + ->register(new ValidatorServiceProvider()); + + $this->register(new TwigServiceProvider(), [ + 'twig.path' => $this->getAppDir().'/views', + ]); + if (true === $this['debug']) { + $this + ->register(new VarDumperServiceProvider()) + ->register(new WebProfilerServiceProvider(), + [ + 'profiler.cache_dir' => $this->getCacheDir().'/profiler', + 'profiler.mount_prefix' => '/_profiler', + ]) + ->register(new ExceptionHandlerServiceProvider()) + ; + } + /* + * Closure supports \Twig_Environment and Silex\Application as a second + * parameter, but we never use Silex\Application so we leave it out. + */ + $r = new \Symfony\Component\HttpFoundation\RequestStack(); + + $this->extend('twig', function (\Twig_Environment $twig) { + $twig->addGlobal('session', $this['session']); + $twig->addExtension(new TranslationExtension(new Translator('en'))); + + return $twig; + }); + } + + /** + * Sets up the rest of the providers for the application. + */ + protected function setUpProviders() + { + $this + + ->register(new CsrfServiceProvider()) + ->register(new FormServiceProvider()) + ->register(new SecurityServiceProvider(), [ + 'security.firewalls' => [ + 'admin' => [ + 'pattern' => '^/admin', + 'http' => true, + ], + ], + ]) + ; + $this->extend('form.extensions', function ($extensions) { + return $extensions; + }); + } + + /** + * Sets up the logger for the application. + */ + protected function setUpLogger() + { + if (true === $this->getDebug()) { + $monologLevel = Logger::DEBUG; + } else { + $monologLevel = Logger::INFO; + } + $this->register(new MonologServiceProvider(), [ + 'monolog.logfile' => $this->getLogDir().'/'.$this->getEnvironment().'.log', + 'monolog.level' => $monologLevel, + ]); + $this->extend('monolog', function (Logger $monolog, Application $app) { + $streamHandler = new StreamHandler($app['monolog.logfile']); + $streamHandler->setFormatter(new ColoredLineFormatter(new TrafficLight())); + $monolog->pushHandler($streamHandler); + + return $monolog; + }); + } + + /** + * Sets up the mailer for the application. + */ + protected function setUpMailer() + { + $this['swiftmailer.options'] = [ + 'host' => 'mx.bgemi.net', + 'port' => '25', + 'username' => null, + 'password' => null, + 'encryption' => null, + 'auth_mode' => null, + ]; + $this->register(new SwiftmailerServiceProvider()); + } + + /** + * @return string + */ + protected function getEnvironment(): string + { + $appEnv = getenv('APP_ENV'); + + if (false === $appEnv) { + return 'development'; + } else { + return $appEnv; + } + } +} diff --git a/app/config/csp.json b/app/config/csp.json new file mode 100644 index 0000000..ab4a3cc --- /dev/null +++ b/app/config/csp.json @@ -0,0 +1,49 @@ +{ + "report-only": false, + "report-uri": "/csp_violation_reporting_endpoint", + "base-uri": [], + "default-src": { + "self": true + }, + "child-src": { + "allow": [ + "https://www.youtube.com", + "https://www.youtube-nocookie.com" + ], + "self": false + }, + "connect-src": { + "self": true + }, + "font-src": { + "self": true, + "data": true + }, + "form-action": { + "allow": [ + "http://localhost.doughnutwedding.com" + ], + "self": true + }, + "frame-ancestors": [], + "img-src": { + "self": true, + "data": true + }, + "media-src": [], + "object-src": [], + "plugin-types": [], + "script-src": { + "allow": [ + "https://www.google-analytics.com" + ], + "self": true, + "unsafe-inline": true, + "unsafe-eval": true + }, + "style-src": { + "self": true, + "unsafe-inline":true + }, + "upgrade-insecure-requests": false +} \ No newline at end of file diff --git a/app/config/database.yml b/app/config/database.yml new file mode 100644 index 0000000..754365f --- /dev/null +++ b/app/config/database.yml @@ -0,0 +1,20 @@ +#connections: +# default: +# connection: +# driver: pdo_mysql +# dbname: doughnut +# password: doughnut +# user: doughnut +# host: mysql +# annotation_autoloaders: +# - 'class_exists' +# metadata_mapping: +# - type: 'Jgut\Slim\Doctrine\ManagerBuilder::METADATA_MAPPING_ANNOTATION' +# path: ['__DIR__ . /../src/Sikofitt/App/Entity'] +dbs.options: + default: + driver: pdo_mysql + host: mysql + dbname: doughnut + user: doughnut + password: doughnut diff --git a/app/views/base.html.twig b/app/views/base.html.twig new file mode 100644 index 0000000..3656cc3 --- /dev/null +++ b/app/views/base.html.twig @@ -0,0 +1,20 @@ + + + + + + Title + + + + + + +{% block debug %}{% endblock %} + +
+ {% include 'flash_messages.html.twig' %} + {% block body %}{% endblock %} +
+ + diff --git a/app/views/flash_messages.html.twig b/app/views/flash_messages.html.twig new file mode 100644 index 0000000..4d72419 --- /dev/null +++ b/app/views/flash_messages.html.twig @@ -0,0 +1,20 @@ +
+ {% for messageType, messages in app.session.flashbag.all %} + {% if messageType == 'error' %} + {% set messageType = 'danger' %} + {% set displayType = 'error' %} + {% elseif messageType == 'info' %} + {% set messageType = 'primary' %} + {% set displayType = 'info' %} + {% else %} + {% set displayType = messageType %} + {% endif %} +
+ +

{{ displayType|capitalize }}

+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endfor %} +
\ No newline at end of file diff --git a/app/views/index.html.twig b/app/views/index.html.twig new file mode 100644 index 0000000..42d57fd --- /dev/null +++ b/app/views/index.html.twig @@ -0,0 +1,8 @@ +{% extends 'base.html.twig' %} + +{% block body %} + {{ 'Hello World!' }} + {% for key, request in app.request_stack %} + {{ dump(key) }} + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/app/views/reset_password.html.twig b/app/views/reset_password.html.twig new file mode 100644 index 0000000..6f3579a --- /dev/null +++ b/app/views/reset_password.html.twig @@ -0,0 +1,21 @@ +{% extends 'base.html.twig' %} + +{% block body %} + {{ form_start(form) }} +
+ Reset Password +
+
+
+ {{ form_row(form.email) }} +
+
+ {{ form_row(form.submit) }} +
+
+
+ {{ form_rest(form) }} +
+ + {{ form_end(form) }} +{% endblock %} \ No newline at end of file diff --git a/app/views/reset_password_confirm.html.twig b/app/views/reset_password_confirm.html.twig new file mode 100644 index 0000000..8880c92 --- /dev/null +++ b/app/views/reset_password_confirm.html.twig @@ -0,0 +1,5 @@ +{% extends 'base.html.twig' %} + +{% block body %} + {% dump(request.flash) %} +{% endblock %} \ No newline at end of file diff --git a/app/views/rsvp_form.html.twig b/app/views/rsvp_form.html.twig new file mode 100644 index 0000000..6a86881 --- /dev/null +++ b/app/views/rsvp_form.html.twig @@ -0,0 +1,70 @@ +{% extends 'base.html.twig' %} + + +{% block body %} + {{ form_start(form) }} +
+ RSVP! + +
+ {{ form_label(form.firstname) }} +
+ {{ form_errors(form.firstname) }} + {{ form_widget(form.firstname) }} +
+
+
+ {{ form_label(form.lastname) }} +
+ {{ form_errors(form.lastname) }} + {{ form_widget(form.lastname) }} +
+
+
+ {{ form_label(form.email) }} +
+ {{ form_errors(form.email) }} + {{ form_widget(form.email) }} +
+
+
+ {{ form_label(form.plainPassword) }} +
+ {{ form_errors(form.plainPassword) }} + {{ form_widget(form.plainPassword) }} +
+
+
+
+ {{ form_label(form.rsvp) }} +
+ {{ form_errors(form.rsvp) }} + {{ form_widget(form.rsvp) }} +
+
+
+
+ ({{ count }} spots available.) +
+
+
+
+ {{ form_label(form.familyside) }} +
+ {{ form_errors(form.familyside) }} + {{ form_widget(form.familyside) }} +
+
+
+ {{ form_errors(form.family) }} + {{ form_label(form.family) }} + {{ form_widget(form.family) }} + +
+
+ +
+
+ {{ form_rest(form) }} + {{ form_end(form) }} +{% endblock %} \ No newline at end of file diff --git a/build/js/doughnut.js b/build/js/doughnut.js index e69de29..3593c78 100644 --- a/build/js/doughnut.js +++ b/build/js/doughnut.js @@ -0,0 +1,3 @@ +jQuery.ready(function($) { + +}); \ No newline at end of file diff --git a/build/less/doughnut.less b/build/less/doughnut.less new file mode 100644 index 0000000..8d5c02f --- /dev/null +++ b/build/less/doughnut.less @@ -0,0 +1,21 @@ +@import "../../vendor/bower/uikit/src/less/uikit.less"; +@import "../../vendor/bower/uikit/src/less/components/variables.less"; + +//@global-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +//@global-font-size: 16px; + +@form-background: #ffffff; + +.uk-input, +.uk-select, +.uk-textarea { + border:1px solid @global-muted-background; + background: #ffffff; +} +.uk-input:focus, +.uk-select:focus, +.uk-textarea:focus { + background: #f5fbfe; + border:1px solid #99baca; + color: #666; +} \ No newline at end of file diff --git a/src/Sikofitt/Http/Kernel/HttpKernel.php b/build/templates/index.html similarity index 100% rename from src/Sikofitt/Http/Kernel/HttpKernel.php rename to build/templates/index.html diff --git a/cli-config.php b/cli-config.php new file mode 100644 index 0000000..3022f6d --- /dev/null +++ b/cli-config.php @@ -0,0 +1,21 @@ + new ConnectionHelper($em->getConnection()), + 'em' => new EntityManagerHelper($em), + +]); + +return $helperSet; diff --git a/composer.json b/composer.json index 80830fe..f20602e 100644 --- a/composer.json +++ b/composer.json @@ -3,32 +3,57 @@ "description": "doughnutwedding.com website", "type": "project", "require": { - "hoa/router": "3.17.01.14", - "twig/twig": "^2.1", - "twig/extensions": "^1.4", + "php":">=7.0", + "bramus/monolog-colored-line-formatter": "~2.0", + "container-interop/container-interop": "^1.1", + "dflydev/doctrine-orm-service-provider": "^2.0", + "doctrine/annotations": "^1.3", + "doctrine/collections": "^1.4", "doctrine/dbal": "^2.5", "doctrine/orm": "^2.5", - "symfony/console": "^3.2", - "pimple/pimple": "^3.0", - "symfony/http-foundation": "^3.2", - "symfony/config": "^3.2", - "symfony/yaml": "^3.2", - "doctrine/annotations": "^1.3", - "tedivm/stash": "^0.14.1", - "doctrine/collections": "^1.4", - "ircmaxell/security-lib": "^1.1", + "egulias/email-validator": "^2.1", + "google/recaptcha": "^1.1", "ircmaxell/random-lib": "^1.2", - "sikofitt/retrorsum": "^1.0" + "ircmaxell/security-lib": "^1.1", + "monolog/monolog": "^1.22", + "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", + "symfony/config": "^3.2", + "symfony/console": "^3.2", + "symfony/form": "^3.2", + "symfony/http-foundation": "^3.2", + "symfony/monolog-bridge": "^3.2", + "symfony/security-csrf": "^3.2", + "symfony/security-guard": "^3.2", + "symfony/security-http": "^3.2", + "symfony/twig-bridge": "^3.2", + "symfony/validator": "^3.2", + "symfony/yaml": "^3.2", + "tedivm/stash": "^0.14.1", + "twig/extensions": "^1.4", + "twig/twig": "^2.1" }, "require-dev": { - "phpunit/phpunit": "^6.0", + "friendsofphp/php-cs-fixer": "^2.0", "fzaninotto/faker": "^1.6", - "friendsofphp/php-cs-fixer": "^2.0" + "phpunit/phpunit": "^6.0", + "silex/providers": "^2.0", + "silex/web-profiler": "^2.0", + "symfony/debug-bundle": "^3.2", + "symfony/var-dumper": "^3.2" + }, "autoload": { - "psr-0": { + "psr-4": { "Sikofitt\\":"src/Sikofitt" - } + }, + "files": ["app/Kernel.php"] }, "license": "GPL-3.0", "authors": [ @@ -37,5 +62,8 @@ "email": "sikofitt@gmail.com" } ], - "minimum-stability": "stable" + "minimum-stability": "stable", + "config": { + "sort-packages": true + } } diff --git a/docker-compose.yml b/docker-compose.yml index 621669b..e239392 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,12 +13,16 @@ services: - php - mysql restart: always + environment: + - "APP_ENV=development" php: build: ./docker/php volumes: - ./:/var/www - ./html:/var/www/html restart: always + environment: + - "APP_ENV=development" mysql: image: mysql:5.7 environment: diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index ae29169..699c458 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -16,6 +16,8 @@ RUN apk update && apk add --no-cache --virtual .build-deps $PHPIZE_DEPS && apk a RUN /usr/local/bin/docker-php-ext-install pdo_mysql intl +RUN echo "y\n"|pecl install scrypt && /usr/local/bin/docker-php-ext-enable scrypt +RUN echo "y\n"|pecl install xdebug && /usr/local/bin/docker-php-ext-enable xdebug RUN apk del .build-deps COPY ./php.ini /usr/local/etc/php \ No newline at end of file diff --git a/doctrine.php b/doctrine.php new file mode 100644 index 0000000..f5e6fad --- /dev/null +++ b/doctrine.php @@ -0,0 +1,30 @@ + [ + 'annotation_autoloaders' => ['class_exists'], + 'connection' => [ + 'driver' => 'pdo_mysql', + 'user' => 'doughnut', + 'password' => 'doughnut', + 'dbname' => 'doughnut', + 'host' => 'mysql', + ], + 'metadata_mapping' => [ + [ + 'type' => ManagerBuilder::METADATA_MAPPING_ANNOTATION, + 'path' => [__DIR__ . '/src/Sikofitt/App/Entity'], + ], + ], + ], +]; + +$managerBuilder = new ManagerBuilder([ManagerBuilder::RELATIONAL_MANAGER_KEY => 'default']); +$managerBuilder->loadSettings($settings); +return $managerBuilder->getManager('entityManager'); + + + diff --git a/html/index.php b/html/index.php index 73a6723..4ace820 100644 --- a/html/index.php +++ b/html/index.php @@ -18,4 +18,38 @@ * along with this program. If not, see . */ -phpinfo(); +use Sikofitt\App\Controller\DefaultController; +use Sikofitt\App\Controller\RsvpController; +use Sikofitt\App\Middleware\CspMiddleware; +use Sikofitt\App\Middleware\HeaderMiddleware; + +$loader = require __DIR__.'/../vendor/autoload.php'; + +$app = new Kernel($loader, true); +// Controllers +// Default +$app->get('/', DefaultController::class.'::indexAction') + ->bind('index'); +//$app->match('/login', DefaultController::class.'loginAction') +// ->bind('login'); +// RSVP Actions +$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->get('/rsvp/reset/{token}', RsvpController::class.'::tokenAction') + ->bind('rsvp_token'); + + //->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); + +$app->before(new HeaderMiddleware(), \Kernel::EARLY_EVENT); +// Run the app +$app->run(); diff --git a/package.json b/package.json index a3e0b89..7ea0150 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,15 @@ "license": "GPL-3.0", "devDependencies": { "grunt": "^1.0.1", + "grunt-contrib-clean": "^1.0.0", + "grunt-contrib-concat": "^1.0.1", + "grunt-contrib-copy": "^1.0.0", "grunt-contrib-cssmin": "^1.0.2", + "grunt-contrib-jshint": "^1.1.0", "grunt-contrib-less": "^1.4.0", + "grunt-contrib-uglify": "^2.1.0", "grunt-contrib-watch": "^1.0.0", + "jshint-stylish": "^2.2.1", "less-plugin-autoprefix": "^1.5.1" } } diff --git a/src/Sikofitt/App/Configuration/DatabaseConfiguration.php b/src/Sikofitt/App/Configuration/DatabaseConfiguration.php new file mode 100644 index 0000000..e7027ca --- /dev/null +++ b/src/Sikofitt/App/Configuration/DatabaseConfiguration.php @@ -0,0 +1,96 @@ +. + */ + +namespace Sikofitt\App\Configuration; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +class DatabaseConfiguration implements ConfigurationInterface +{ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root('doughnut'); + + $rootNode->children() + ->arrayNode('connections') + ->prototype('array') + ->children() + ->arrayNode('connection') + ->children() + ->scalarNode('driver') + ->isRequired() + ->validate() + ->ifNotInArray(['pdo_mysql', 'pdo_pgsql', 'pdo_sqlite']) + ->thenInvalid('Invalid driver : %s') + ->end() // ifNotInArray + ->end() // driver + ->scalarNode('dbname')->isRequired()->end() // database + ->scalarNode('host')->defaultValue('127.0.0.1')->end() + ->scalarNode('user')->isRequired()->end() + ->scalarNode('password')->isRequired()->end() + ->end() // connection.prototype + ->end() // connection + ->arrayNode('annotation_autoloaders') + ->requiresAtLeastOneElement() + ->prototype('scalar') + ->isRequired() + ->end() + ->end() + ->arrayNode('metadata_mapping') + ->prototype('array') + ->children() + ->arrayNode('path') + ->requiresAtLeastOneElement() + ->prototype('scalar') + ->isRequired() + ->end() + ->end() + ->scalarNode('type') + ->isRequired() + ->beforeNormalization() + ->ifString() + ->then(function ($s) { + return $this->normalizeConstant($s); + }) + ->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + + return $treeBuilder; + } + + private function normalizeConstant($const) + { + $classParts = explode('::', $const); + if (isset($classParts[1])) { + $reflected = new \ReflectionClass($classParts[0]); + $constant = $reflected->getConstant($classParts[1]); + + return $constant; + } else { + return $const; + } + } +} diff --git a/src/Sikofitt/App/Controller/DefaultController.php b/src/Sikofitt/App/Controller/DefaultController.php new file mode 100644 index 0000000..09a4044 --- /dev/null +++ b/src/Sikofitt/App/Controller/DefaultController.php @@ -0,0 +1,75 @@ +. + */ + +namespace Sikofitt\App\Controller; + +use Sikofitt\App\Form\RsvpType; +use Symfony\Component\Form\Extension\Csrf\CsrfExtension; +use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension; +use Symfony\Component\Form\Forms; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Csrf\CsrfTokenManager; + +class DefaultController +{ + public function indexAction(Request $request, \Kernel $app) + { + return $app->render('index.html.twig', ['request' => $request]); + } + + public function rsvpAction() + { + /* $app = $this->app; + $rsvp = new Rsvp(); + $rsvp + ->setGuests(2) + ->setCreated(new \DateTime('now')) + ->setUpdated(new \DateTime('now')); + + $user = new User(); + $user->setFirstName('Eric') + ->setLastName('Wheeler') + ->setFamily(true) + ->setEmail('sikofitt@gmail.com') + ->setCreated(new \DateTime('now')) + ->setUpdated(new \DateTime('now')) + ->setFamilySide(User::ERIC_SIDE) + ->setRsvp($rsvp); + + + $app['em']->persist($user); + $app['em']->flush(); */ + $bytes = \ParagonIE_Sodium_Compat::randombytes_buf(22); + + $password = new Password(new ScryptPassword()); + // dump($password->hash('password')); + $blake = \ParagonIE_Sodium_Compat::crypto_generichash($bytes); + $blake2b = \ParagonIE_Sodium_Core_BLAKE2b::bin2hex($blake); + + $formFactory = Forms::createFormFactoryBuilder() + ->addTypeExtension(new FormTypeHttpFoundationExtension()) + ->addExtension(new CsrfExtension(new CsrfTokenManager())) + ->getFormFactory(); + $form = $formFactory->create(RsvpType::class); + // dump($form->createView()); + return 'hello'; + //return $this->container->get('view')->render('RsvpForm.html.twig', ['form' => $form->createView()]); + } +} diff --git a/src/Sikofitt/App/Controller/RsvpController.php b/src/Sikofitt/App/Controller/RsvpController.php new file mode 100644 index 0000000..5c4b5e6 --- /dev/null +++ b/src/Sikofitt/App/Controller/RsvpController.php @@ -0,0 +1,160 @@ +. + */ + +namespace Sikofitt\App\Controller; + +use Doctrine\ORM\EntityManager; +use Sikofitt\{ + App\Entity\Rsvp, App\Entity\User, App\Form\ResetType, App\Form\RsvpType, App\Repository\RsvpRepository, App\Repository\UserRepository +}; + +use Symfony\Component\Form\FormFactory; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Validator\ConstraintViolationList; + +class RsvpController +{ + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Kernel $app + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response + */ + public function indexAction(Request $request, \Kernel $app) + { + /** + * @var EntityManager $em + * @var RsvpRepository $rsvpRepo + * @var UserRepository $userRepo + */ + $em = $app['orm.em']; + $rsvpRepo = $em->getRepository('Sikofitt\App\Entity\Rsvp'); + $count = (40 - $rsvpRepo->getRsvpCount()); + $userRepo = $em->getRepository('Sikofitt\App\Entity\User'); + $kCount = $userRepo->getKatrinaCount(); + $eCount = $userRepo->getEricCount(); + /** + * @var FormFactory $formFactory + */ + $formFactory = $app['form.factory']; + $user = new User(); + $rsvp = new Rsvp(); + $rsvp + ->setCreated(new \DateTime('now')) + ->setUpdated(new \DateTime('now')); + $user + ->setRsvp($rsvp) + ->setCreated(new \DateTime('now')) + ->setUpdated(new \DateTime('now')); + + $form = $formFactory->create(RsvpType::class, $user); + if ($request->isMethod('POST')) { + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $user = $form->getData(); + $user->setPassword($user->getPlainPassword()); + /** + * @var EntityManager $em + */ + $em = $app['orm.em']; + $em->persist($user); + $em->flush(); + + return $app->redirect('/rsvp'); + } + } + + return $app->render( + 'rsvp_form.html.twig', + [ + 'form' => $form->createView(), + 'count' => $count, + 'kCount' => $kCount, + 'eCount' => $eCount, + ] + ); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Kernel $app + * @param string $token + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function resetAction(Request $request, \Kernel $app) + { + $formFactory = $app->getFormFactory(); + $passwordResetForm = $formFactory + ->createBuilder(ResetType::class) + ->getForm(); + $update = false; + if ($request->isMethod('POST')) { + $passwordResetForm->handleRequest($request); + if ($passwordResetForm->isSubmitted() && $passwordResetForm->isValid()) { + $data = $passwordResetForm->get('email')->getData(); + $update = $app['orm.em']->getRepository('Sikofitt\App\Entity\User') + ->setResetToken($data); + } else { + $data = null; + } + } else { + $data = null; + } + /** + * @var UserRepository $userRepo + */ + $userRepo = $app['orm.em']->getRepository('Sikofitt\App\Entity\User'); + + //$emailResult = $userRepo->getEmail($passwordResetForm->get('email')); + /*if(null === $emailResult) { + return $app->render('reset_password_confirm.html.twig', [ + 'message' => 'Email was not found in database', + ]); + } elseif($emailResult instanceof ConstraintViolationList) { + return $app->render( + 'reset_password.html.twig', + [ + 'form' => $passwordResetForm->createView(), + 'data' => $data, + 'email' => $emailResult, + 'message' => $emailResult, + ] + ); + } else { + return $app->render('reset_password_confirm.html.twig', [ + 'message' => 'Please check your email. A reset request has been sent.', + ]); + }*/ + $app->addInfo('Message', 'message 2'); + return $app->render( + 'reset_password.html.twig', + [ + 'form' => $passwordResetForm->createView(), + 'data' => $data, + //'email' => $emailResult, + ] + ); + } + + public function tokenAction(Request $request, \Kernel $app, string $token = null) + { + } +} diff --git a/src/Sikofitt/App/Entity/Rsvp.php b/src/Sikofitt/App/Entity/Rsvp.php new file mode 100644 index 0000000..2516d1e --- /dev/null +++ b/src/Sikofitt/App/Entity/Rsvp.php @@ -0,0 +1,183 @@ +. + */ + +namespace Sikofitt\App\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Class Rsvp. + * + * @ORM\Table(name="rsvps") + * @ORM\Entity(repositoryClass="Sikofitt\App\Repository\RsvpRepository") + */ +class Rsvp +{ + /** + * @var int $id + * @ORM\Id() + * @ORM\Column(name="id", type="integer", unique=true, nullable=false) + * @ORM\GeneratedValue(strategy="IDENTITY") + * @Assert\NotBlank() + * @Assert\Regex(pattern="'/\d+/'") + */ + private $id; + + /** + * @var int + * @ORM\OneToOne(targetEntity="Sikofitt\App\Entity\User", mappedBy="rsvp", cascade={"persist"}) + */ + private $user; + + /** + * @var int + * @ORM\Column(name="guests", nullable=true, type="integer") + * @Assert\Regex(pattern="'/\d+/'") + * @Assert\Range(min="1", max="2") + */ + private $guests; + + /** + * @var \DateTime + * @ORM\Column(name="created", type="datetime") + */ + private $created = null; + + /** + * @var \DateTime + * @ORM\Column(name="updated", type="datetime") + */ + private $updated = null; + + public function __construct() + { + if (null === $this->created) { + $this->created = new \DateTime('now'); + } + $this->updated = new \DateTime('now'); + } + + /** + * Get id. + * + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * Set guests. + * + * @param int $guests + * + * @return Rsvp + */ + public function setGuests($guests) + { + $this->guests = $guests; + + return $this; + } + + /** + * Get guests. + * + * @return int + */ + public function getGuests() + { + return $this->guests; + } + + /** + * Set created. + * + * @param \DateTime $created + * + * @return Rsvp + */ + public function setCreated($created) + { + $this->created = $created; + + return $this; + } + + /** + * Get created. + * + * @return \DateTime + */ + public function getCreated() + { + return $this->created; + } + + /** + * Set updated. + * + * @param \DateTime $updated + * + * @return Rsvp + */ + public function setUpdated($updated) + { + $this->updated = $updated; + + return $this; + } + + /** + * Get updated. + * + * @return \DateTime + */ + public function getUpdated() + { + return $this->updated; + } + + /** + * Set user. + * + * @param \Sikofitt\App\Entity\User $user + * + * @return Rsvp + */ + public function setUser(\Sikofitt\App\Entity\User $user = null) + { + $this->user = $user; + + return $this; + } + + /** + * Get user. + * + * @return \Sikofitt\App\Entity\User + */ + public function getUser() + { + return $this->user; + } +} diff --git a/src/Sikofitt/App/Entity/User.php b/src/Sikofitt/App/Entity/User.php new file mode 100644 index 0000000..b6f58ed --- /dev/null +++ b/src/Sikofitt/App/Entity/User.php @@ -0,0 +1,390 @@ +. + */ + +namespace Sikofitt\App\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Class User. + * + * @ORM\Entity(repositoryClass="Sikofitt\App\Repository\UserRepository") + * @ORM\Table(name="users") + */ +class User +{ + const KATRINA_SIDE = 'Katrina'; + + const ERIC_SIDE = 'Eric'; + + /** + * @ORM\Id() + * @ORM\Column(name="id", type="integer", nullable=false, unique=true) + * @ORM\GeneratedValue(strategy="IDENTITY") + * + * @var int + */ + private $id; + + /** + * @ORM\Column(name="first_name", type="string", length=255, nullable=false) + * @Assert\NotBlank() + * @Assert\Regex(pattern="/\w+/") + */ + private $firstName; + + /** + * @ORM\Column(name="last_name", type="string", length=255, nullable=false) + * @Assert\NotBlank() + * @Assert\Regex(pattern="/\w+/") + */ + private $lastName; + + /** + * @ORM\Column(type="boolean", name="is_family", nullable=false) + * @Assert\Type(type="bool") + */ + private $family = false; + + /** + * @ORM\Column(type="string", name="family_side", nullable=true) + * @Assert\Choice(choices="{self::KATRINA_SIDE, self::ERIC_SIDE}", multiple=false) + * + * @var null|string + */ + private $familySide = null; + + /** + * @ORM\Column(name="email", type="string", length=255) + * @Assert\Email(strict=true, checkHost=true, checkMX=true) + */ + private $email; + + /** + * @var string + * @ORM\Column(name="password", type="string", length=255)) + */ + private $password; + + /** + * @var string + */ + private $plainPassword; + + /** + * @var string + * @ORM\Column(name="token", type="string", length=255, nullable=true) + */ + private $token; + + /** + * @var int + * @ORM\OneToOne(targetEntity="Sikofitt\App\Entity\Rsvp", inversedBy="user", cascade={"persist"}) + */ + private $rsvp; + /** + * @ORM\Column(type="datetime", name="created") + */ + private $created = null; + + /** + * @ORM\Column(type="datetime", name="updated") + */ + private $updated = null; + + public function __construct() + { + if (null === $this->created) { + $this->created = new \DateTime('now'); + } + $this->updated = new \DateTime('now'); + } + + /** + * Get id. + * + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * Set firstName. + * + * @param string $firstName + * + * @return User + */ + public function setFirstName($firstName) + { + $this->firstName = $firstName; + + return $this; + } + + /** + * Get firstName. + * + * @return string + */ + public function getFirstName() + { + return $this->firstName; + } + + /** + * Set lastName. + * + * @param string $lastName + * + * @return User + */ + public function setLastName($lastName) + { + $this->lastName = $lastName; + + return $this; + } + + /** + * Get lastName. + * + * @return string + */ + public function getLastName() + { + return $this->lastName; + } + + /** + * Set family. + * + * @param bool $family + * + * @return User + */ + public function setFamily($family) + { + $this->family = $family; + + return $this; + } + + /** + * Get family. + * + * @return bool + */ + public function getFamily() + { + return $this->family; + } + + /** + * Set familySide. + * + * @param string $familySide + * + * @return User + */ + public function setFamilySide($familySide) + { + $this->familySide = $familySide; + + return $this; + } + + /** + * Get familySide. + * + * @return string + */ + public function getFamilySide() + { + return $this->familySide; + } + + /** + * Set email. + * + * @param string $email + * + * @return User + */ + public function setEmail($email) + { + $this->email = $email; + + return $this; + } + + /** + * Get email. + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * @param string $password + * + * @return $this + */ + public function setPassword($password) + { + $encoder = new BCryptPasswordEncoder(14); + + $salt = bin2hex(random_bytes(16)); + $this->password = $encoder->encodePassword($password, $salt); + + return $this; + } + + /** + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * @return string + */ + public function getPlainPassword(): string + { + if (null === $this->plainPassword) { + return ''; + } else { + return $this->plainPassword; + } + } + + /** + * @param string $plainPassword + * + * @return User + */ + public function setPlainPassword(string $plainPassword): User + { + $this->plainPassword = $plainPassword; + + return $this; + } + + /** + * @param string $token + * + * @return $this + */ + public function setToken($token) + { + $this->token = $token; + + return $this; + } + + /** + * @return string + */ + public function getToken() + { + return $this->token; + } + + /** + * Set created. + * + * @param \DateTime $created + * + * @return User + */ + public function setCreated($created) + { + $this->created = $created; + + return $this; + } + + /** + * Get created. + * + * @return \DateTime + */ + public function getCreated() + { + return $this->created; + } + + /** + * Set updated. + * + * @param \DateTime $updated + * + * @return User + */ + public function setUpdated($updated) + { + $this->updated = $updated; + + return $this; + } + + /** + * Get updated. + * + * @return \DateTime + */ + public function getUpdated() + { + return $this->updated; + } + + /** + * Set rsvp. + * + * @param \Sikofitt\App\Entity\Rsvp $rsvp + * + * @return User + */ + public function setRsvp(\Sikofitt\App\Entity\Rsvp $rsvp = null) + { + $this->rsvp = $rsvp; + + return $this; + } + + /** + * Get rsvp. + * + * @return \Sikofitt\App\Entity\Rsvp + */ + public function getRsvp() + { + return $this->rsvp; + } +} diff --git a/src/Sikofitt/App/Form/ResetType.php b/src/Sikofitt/App/Form/ResetType.php new file mode 100644 index 0000000..1fdf729 --- /dev/null +++ b/src/Sikofitt/App/Form/ResetType.php @@ -0,0 +1,78 @@ +. + */ + +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\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\NotBlank; + +/** + * Class ResetType. + */ +class ResetType extends AbstractType +{ + /** + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('email', EmailType::class, [ + 'attr' => [ + 'class' => 'uk-input uk-form-large uk-padding-small uk-box-shadow-hover-small', + 'placeholder' => 'Email address', + ], + 'label' => 'Email address', + 'label_attr' => [ + 'class' => 'uk-form-label uk-text-primary uk-hidden', + ], + 'constraints' => [ + new NotBlank(), + new Email([ + 'strict' => true, + 'checkMX' => true, + 'checkHost' => true, + ]), + ], + ]) + ->add('submit', SubmitType::class, [ + 'attr' => [ + 'class' => 'uk-button uk-button-large uk-button-primary uk-width-1-1@s', + ], + ]) + ; + } + + /** + * @param \Symfony\Component\OptionsResolver\OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('attr', [ + 'class' => 'uk-form uk-margin-large', + ]); + } +} diff --git a/src/Sikofitt/App/Form/RsvpType.php b/src/Sikofitt/App/Form/RsvpType.php new file mode 100644 index 0000000..2d87d8e --- /dev/null +++ b/src/Sikofitt/App/Form/RsvpType.php @@ -0,0 +1,141 @@ +. + */ + +namespace Sikofitt\App\Form; + +use Sikofitt\App\Entity\Rsvp; +use Sikofitt\App\Entity\User; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\Type\{ + CheckboxType, + ChoiceType, + EmailType, + IntegerType, + PasswordType, + RadioType, + TextType +}; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class RsvpType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('firstname', TextType::class, [ + 'attr' => [ + 'class' => 'uk-input uk-form-large uk-padding-small uk-box-shadow-hover-small', + 'placeholder' => 'First Name', + ], + 'label' => 'First name', + 'label_attr' => [ + 'class' => 'uk-form-label uk-text-primary', + ], + ]) + ->add('lastname', TextType::class, [ + 'attr' => [ + 'class' => 'uk-input uk-form-large uk-padding-small uk-box-shadow-hover-small', + 'placeholder' => 'Last Name', + ], + 'label' => 'Last name', + 'label_attr' => [ + 'class' => 'uk-form-label uk-text-primary', + ], + ]) + ->add('email', EmailType::class, [ + 'attr' => [ + 'class' => 'uk-input uk-form-large uk-padding-small uk-box-shadow-hover-small', + 'placeholder' => 'Email address', + ], + 'label' => 'Email address', + 'label_attr' => [ + 'class' => 'uk-form-label uk-text-primary', + ], + ]) + ->add('plainPassword', PasswordType::class, [ + 'attr' => [ + 'class' => 'uk-input uk-form-large uk-padding-small uk-box-shadow-hover-small', + 'placeholder' => 'Password', + ], + 'label' => 'Password', + 'label_attr' => [ + 'class' => 'uk-form-label uk-text-primary', + ], + ]) + ->add('rsvp', IntegerType::class, [ + 'attr' => [ + 'class' => 'uk-input uk-form-large uk-padding-small uk-form-width-medium uk-box-shadow-hover-small', + 'placeholder' => 'Number of Guests (including yourself)', + ], + 'label' => 'Number of guests? (including yourself)', + 'label_attr' => [ + 'class' => 'uk-form-label uk-text-primary', + ], + ]) + ->add('familyside', ChoiceType::class, [ + 'choices' => [ + User::ERIC_SIDE => User::ERIC_SIDE, + User::KATRINA_SIDE => User::KATRINA_SIDE, + ], + 'attr' => [ + 'class' => 'uk-select uk-form-large uk-box-shadow-hover-small', + 'style' => 'padding-left:16px;', + ], + 'label' => 'Who are you coming for?', + 'label_attr' => [ + 'class' => 'uk-form-label uk-text-primary', + ], + ]) + ->add('family', CheckboxType::class, [ + 'label' => 'Are you an immediate family member?', + 'required' => false, + 'attr' => [ + 'class' => 'uk-checkbox uk-box-shadow-hover-small', + ], + 'label_attr' => [ + 'class' => 'uk-margin-right', + ], + ]); + $builder->get('rsvp') + ->addModelTransformer(new CallbackTransformer( + function ($rsvp) { + if (null === $rsvp) { + return $rsvp; + } else { + return $rsvp->getGuests(); + } + }, + function (Int $rsvpInt) { + $rsvp = new Rsvp(); + $rsvp->setGuests($rsvpInt); + + return $rsvp; + } + )); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', User::class); + $resolver->setDefault('attr', ['class' => 'uk-form-horizontal uk-margin-large']); + } +} diff --git a/src/Sikofitt/App/Middleware/CspMiddleware.php b/src/Sikofitt/App/Middleware/CspMiddleware.php new file mode 100644 index 0000000..7eb69f5 --- /dev/null +++ b/src/Sikofitt/App/Middleware/CspMiddleware.php @@ -0,0 +1,56 @@ +. + */ + +namespace Sikofitt\App\Middleware; + +use Monolog\Logger; +use ParagonIE\CSPBuilder\CSPBuilder; +use Symfony\Component\HttpFoundation\Request; + +/** + * Class CspMiddleware. + * + * Builds Content Security Policy (CSP) headers. + */ +class CspMiddleware +{ + public function __invoke(Request $request, \Kernel $app) + { + $cspDir = realpath($app->getConfigDir()); + if (false === file_exists($cspDir.'/csp.json')) { + $app->log( + sprintf('csp.json was not found in %s, skipping.', $cspDir), + [ + 'configured log dir' => realpath($cspDir), + ], + Logger::NOTICE + ); + + return; + } + $app->log('Setting Content Security Policy (CSP) headers.', + [ + 'class' => get_class($this), + ] + ); + $csp = CSPBuilder::fromFile($app->getBaseDir().'/app/config/csp.json'); + $csp->sendCSPHeader(); + } +} diff --git a/src/Sikofitt/App/Middleware/HeaderMiddleware.php b/src/Sikofitt/App/Middleware/HeaderMiddleware.php new file mode 100644 index 0000000..b9893cd --- /dev/null +++ b/src/Sikofitt/App/Middleware/HeaderMiddleware.php @@ -0,0 +1,44 @@ +. + */ + +namespace Sikofitt\App\Middleware; + +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; + +/** + * Class HeaderMiddleware. + * + * Injects custom headers into the application. + */ +class HeaderMiddleware +{ + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Silex\Application $app + */ + public function __invoke(Request $request, Application $app) + { + $poweredByLine = sprintf('Silex/%s [%s] %s/%s', Application::VERSION, php_sapi_name(), php_uname('s'), php_uname('m')); + + header('X-Powered-By: '.$poweredByLine); + header('Server: Nginx/Unix ('.php_uname('m').')'); + } +} diff --git a/src/Sikofitt/App/Repository/RsvpRepository.php b/src/Sikofitt/App/Repository/RsvpRepository.php new file mode 100644 index 0000000..c62baec --- /dev/null +++ b/src/Sikofitt/App/Repository/RsvpRepository.php @@ -0,0 +1,34 @@ +. + */ + +namespace Sikofitt\App\Repository; + +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query; + +class RsvpRepository extends EntityRepository +{ + public function getRsvpCount() + { + return $this->createQueryBuilder('r') + ->select('sum(r.guests)') + ->getQuery()->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + } +} diff --git a/src/Sikofitt/App/Repository/UserRepository.php b/src/Sikofitt/App/Repository/UserRepository.php new file mode 100644 index 0000000..7304c13 --- /dev/null +++ b/src/Sikofitt/App/Repository/UserRepository.php @@ -0,0 +1,94 @@ +. + */ + +namespace Sikofitt\App\Repository; + +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query; +use Sikofitt\App\Entity\User; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Validation; + +/** + * Class UserRepository. + * + * Doctrine repository for User entity. + */ +class UserRepository extends EntityRepository +{ + public function getKatrinaCount() + { + return $this->createQueryBuilder('u') + ->select('count(u.familySide)') + ->distinct(true) + ->where('u.familySide = :side') + ->setParameter('side', User::KATRINA_SIDE) + ->getQuery() + ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + } + + public function getEricCount() + { + return $this->createQueryBuilder('u') + ->select('count(u.familySide)') + ->distinct(true) + ->where('u.familySide = :side') + ->setParameter('side', User::ERIC_SIDE) + ->getQuery() + ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + } + + public function getEmail(string $email) + { + $validator = Validation::createValidator(); + $emailConstraint = new Email([ + 'checkMX' => true, + 'checkHost' => true, + 'strict' => true, + ]); + + $result = $validator->validate($email, [$emailConstraint]); + + if ($result->count() > 0) { + return $result; + } + + return $this->createQueryBuilder('u') + ->select('u.email') + ->where('u.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + } + + public function setResetToken(string $email) + { + $token = bin2hex(random_bytes(22)); + + return (bool) $this->createQueryBuilder('u') + ->update() + ->set('u.token', ':token') + ->setParameter('token', $token) + ->where('u.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); + } +} diff --git a/src/Sikofitt/App/Traits/FlashTrait.php b/src/Sikofitt/App/Traits/FlashTrait.php new file mode 100644 index 0000000..5145e48 --- /dev/null +++ b/src/Sikofitt/App/Traits/FlashTrait.php @@ -0,0 +1,96 @@ +. + */ + +namespace Sikofitt\App\Traits; + +/** + * Trait FlashTrait. + * + * Adds shortcuts for adding flash messages. + */ +trait FlashTrait +{ + /** + * @param \string[] ...$messages + * + * @return $this + */ + public function addInfo(string ...$messages) + { + if (false === isset($this['session'])) { + return; + } + + foreach ($messages as $message) { + $this['session']->getFlashBag()->add('info', $message); + } + + return $this; + } + + /** + * @param \string[] ...$messages + * + * @return $this + */ + public function addError(string ...$messages) + { + if (false === isset($this['session'])) { + return; + } + + foreach ($messages as $message) { + $this['session']->getFlashBag()->add('error', $message); + } + + return $this; + } + + /** + * @param \string[] ...$messages + * + * @return $this + */ + public function addSuccess(string ...$messages) + { + if (false === isset($this['session'])) { + return; + } + + foreach ($messages as $message) { + $this['session']->getFlashBag()->add('success', $message); + } + + return $this; + } + + public function addWarning(string ...$messages) + { + if (false === isset($this['session'])) { + return; + } + + foreach ($messages as $message) { + $this['session']->getFlashBag()->add('warning', $message); + } + + return $this; + } +} diff --git a/src/Sikofitt/Security/MysqlAuthenticator.php b/src/Sikofitt/Security/MysqlAuthenticator.php new file mode 100644 index 0000000..f4cf3c7 --- /dev/null +++ b/src/Sikofitt/Security/MysqlAuthenticator.php @@ -0,0 +1,198 @@ +. + */ + +namespace Sikofitt\Security; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; + +class MysqlAuthenticator extends AbstractGuardAuthenticator +{ + /** + * Returns a response that directs the user to authenticate. + * + * This is called when an anonymous request accesses a resource that + * requires authentication. The job of this method is to return some + * response that "helps" the user start into the authentication process. + * + * Examples: + * A) For a form login, you might redirect to the login page + * return new RedirectResponse('/login'); + * B) For an API token authentication system, you return a 401 response + * return new Response('Auth header required', 401); + * + * @param Request $request The request that resulted + * in an + * AuthenticationException + * @param AuthenticationException $authException The exception that started + * the authentication process + * + * @return Response + */ + public function start( + Request $request, + AuthenticationException $authException = null + ) { + // TODO: Implement start() method. + } + + /** + * Get the authentication credentials from the request and return them + * as any type (e.g. an associate array). If you return null, + * authentication + * will be skipped. + * + * Whatever value you return here will be passed to getUser() and + * checkCredentials() + * + * For example, for a form login, you might: + * + * if ($request->request->has('_username')) { + * return array( + * 'username' => $request->request->get('_username'), + * 'password' => $request->request->get('_password'), + * ); + * } else { + * return; + * } + * + * Or for an API token that's on a header, you might use: + * + * return array('api_key' => $request->headers->get('X-API-TOKEN')); + * + * @param Request $request + * + * @return mixed|null + */ + public function getCredentials(Request $request) + { + // TODO: Implement getCredentials() method. + } + + /** + * Return a UserInterface object based on the credentials. + * + * The *credentials* are the return value from getCredentials() + * + * You may throw an AuthenticationException if you wish. If you return + * null, then a UsernameNotFoundException is thrown for you. + * + * @param mixed $credentials + * @param UserProviderInterface $userProvider + * + * @throws AuthenticationException + * + * @return UserInterface|null + */ + public function getUser($credentials, UserProviderInterface $userProvider) + { + // TODO: Implement getUser() method. + } + + /** + * Returns true if the credentials are valid. + * + * If any value other than true is returned, authentication will + * fail. You may also throw an AuthenticationException if you wish + * to cause authentication to fail. + * + * The *credentials* are the return value from getCredentials() + * + * @param mixed $credentials + * @param UserInterface $user + * + * @throws AuthenticationException + * + * @return bool + */ + public function checkCredentials($credentials, UserInterface $user) + { + // TODO: Implement checkCredentials() method. + } + + /** + * Called when authentication executed, but failed (e.g. wrong username + * password). + * + * This should return the Response sent back to the user, like a + * RedirectResponse to the login page or a 403 response. + * + * If you return null, the request will continue, but the user will + * not be authenticated. This is probably not what you want to do. + * + * @param Request $request + * @param AuthenticationException $exception + * + * @return Response|null + */ + public function onAuthenticationFailure( + Request $request, + AuthenticationException $exception + ) { + // TODO: Implement onAuthenticationFailure() method. + } + + /** + * Called when authentication executed and was successful! + * + * This should return the Response sent back to the user, like a + * RedirectResponse to the last page they visited. + * + * If you return null, the current request will continue, and the user + * will be authenticated. This makes sense, for example, with an API. + * + * @param Request $request + * @param TokenInterface $token + * @param string $providerKey The provider (i.e. firewall) key + * + * @return Response|null + */ + public function onAuthenticationSuccess( + Request $request, + TokenInterface $token, + $providerKey + ) { + // TODO: Implement onAuthenticationSuccess() method. + } + + /** + * Does this method support remember me cookies? + * + * Remember me cookie will be set if *all* of the following are met: + * A) This method returns true + * B) The remember_me key under your firewall is configured + * C) The "remember me" functionality is activated. This is usually + * done by having a _remember_me checkbox in your form, but + * can be configured by the "always_remember_me" and + * "remember_me_parameter" parameters under the "remember_me" firewall + * key + * + * @return bool + */ + public function supportsRememberMe() + { + // TODO: Implement supportsRememberMe() method. + } +} diff --git a/src/Sikofitt/Security/ScryptEncoder.php b/src/Sikofitt/Security/ScryptEncoder.php new file mode 100644 index 0000000..c0077ee --- /dev/null +++ b/src/Sikofitt/Security/ScryptEncoder.php @@ -0,0 +1,96 @@ +. + */ + +namespace Sikofitt\Security; + +use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; + +class ScryptEncoder implements PasswordEncoderInterface +{ + /** + * Encodes the raw password. + * + * @param string $raw The password to encode + * @param string $salt The salt + * + * @return string The encoded password + */ + public function encodePassword($raw, $salt) + { + $salt = $this->generateSalt(); + $hash = \scrypt($raw, $salt, 16384, 8, 1, 32); + + return 16384 .'$'. 8 .'$'. 1 .'$'.$salt.'$'.$hash; + } + + /** + * Checks a raw password against an encoded password. + * + * @param string $encoded An encoded password + * @param string $raw A raw password + * @param string $salt The salt + * + * @return bool true if the password is valid, false otherwise + */ + public function isPasswordValid($encoded, $raw, $salt) + { + $salt = null; + // Is there actually a hash? + if (!$encoded) { + return false; + } + list($N, $r, $p, $salt, $hash) = explode('$', $encoded); + // No empty fields? + if (empty($N) or empty($r) or empty($p) or empty($salt) or empty($hash)) { + return false; + } + // Are numeric values numeric? + if (!is_numeric($N) or !is_numeric($r) or !is_numeric($p)) { + return false; + } + $calculated = \scrypt($raw, $salt, $N, $r, $p, 32); + // Use compareStrings to avoid timeing attacks + return $this->compareStrings($hash, $calculated); + } + + private function compareStrings($expected, $actual) + { + $expected = (string) $expected; + $actual = (string) $actual; + $lenExpected = \mb_strlen($expected); + $lenActual = \mb_strlen($actual); + $len = min($lenExpected, $lenActual); + $result = 0; + for ($i = 0; $i < $len; ++$i) { + $result |= ord($expected[$i]) ^ ord($actual[$i]); + } + $result |= $lenExpected ^ $lenActual; + + return $result === 0; + } + + private function generateSalt() + { + $buffer = random_bytes(8); + $salt = str_replace(['+', '$'], ['.', ''], base64_encode($buffer)); + + return $salt; + } +}