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) }}
+
+
+
+ {{ 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) }}
+
+ {{ 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;
+ }
+}