From 3ca0abe147bd99b43fccb28652980f958f2966b9 Mon Sep 17 00:00:00 2001 From: Samuel ORTION Date: Thu, 18 Aug 2022 21:08:03 +0200 Subject: [PATCH] www: Add dedicated database for Users (separated from birdnet birds data) --- www/.env | 4 + www/composer.json | 9 +- www/composer.lock | 1388 ++++++++++++++++- www/config/bundles.php | 3 + www/config/packages/doctrine.yaml | 11 +- www/config/packages/mailer.yaml | 3 + www/config/packages/security.yaml | 43 + www/config/packages/validator.yaml | 13 + www/config/packages/web_profiler.yaml | 17 + www/config/routes/web_profiler.yaml | 8 + www/config/services.yaml | 3 + www/docker-compose.override.yml | 6 + www/migrations/Version20220818163807.php | 33 + www/migrations/Version20220818165607.php | 44 + www/src/AppBundle/Connections.php | 10 + www/src/Controller/AuthController.php | 24 +- www/src/Controller/HomeController.php | 9 +- www/src/Controller/RecordsController.php | 9 +- www/src/Controller/RegistrationController.php | 90 ++ www/src/Controller/ServicesController.php | 2 +- www/src/Controller/StatsController.php | 11 +- www/src/Controller/TodayController.php | 26 +- www/src/Entity/User.php | 131 ++ www/src/Form/RegistrationFormType.php | 55 + www/src/Repository/UserRepository.php | 83 + www/src/Security/EmailVerifier.php | 52 + www/symfony.lock | 52 + www/templates/menu.html.twig | 9 - .../registration/confirmation_email.html.twig | 11 + www/templates/registration/register.html.twig | 21 + www/templates/utils/locale-switcher.html.twig | 5 +- www/templates/utils/nav-item.html.twig | 2 +- 32 files changed, 2143 insertions(+), 44 deletions(-) create mode 100644 www/config/packages/mailer.yaml create mode 100644 www/config/packages/security.yaml create mode 100644 www/config/packages/validator.yaml create mode 100644 www/config/packages/web_profiler.yaml create mode 100644 www/config/routes/web_profiler.yaml create mode 100644 www/migrations/Version20220818163807.php create mode 100644 www/migrations/Version20220818165607.php create mode 100644 www/src/AppBundle/Connections.php create mode 100644 www/src/Controller/RegistrationController.php create mode 100644 www/src/Entity/User.php create mode 100644 www/src/Form/RegistrationFormType.php create mode 100644 www/src/Repository/UserRepository.php create mode 100644 www/src/Security/EmailVerifier.php create mode 100644 www/templates/registration/confirmation_email.html.twig create mode 100644 www/templates/registration/register.html.twig diff --git a/www/.env b/www/.env index dbb8dad..5983721 100644 --- a/www/.env +++ b/www/.env @@ -31,3 +31,7 @@ DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&ch ### records folder RECORDS_DIR=%kernel.project_dir%/../var/chunks # adapt to your needs ### + +###> symfony/mailer ### +# MAILER_DSN=null://null +###< symfony/mailer ### diff --git a/www/composer.json b/www/composer.json index 1037790..07df82e 100644 --- a/www/composer.json +++ b/www/composer.json @@ -14,13 +14,18 @@ "symfony/console": "6.1.*", "symfony/dotenv": "6.1.*", "symfony/flex": "^2", + "symfony/form": "6.1.*", "symfony/framework-bundle": "6.1.*", + "symfony/mailer": "6.1.*", "symfony/proxy-manager-bridge": "6.1.*", "symfony/runtime": "6.1.*", + "symfony/security-bundle": "6.1.*", "symfony/translation": "6.1.*", "symfony/twig-bundle": "6.1.*", + "symfony/validator": "6.1.*", "symfony/webpack-encore-bundle": "^1.15", "symfony/yaml": "6.1.*", + "symfonycasts/verify-email-bundle": "^1.11", "twig/extra-bundle": "^2.12|^3.0", "twig/intl-extra": "^3.4", "twig/string-extra": "^3.4", @@ -79,6 +84,8 @@ } }, "require-dev": { - "symfony/maker-bundle": "^1.45" + "symfony/maker-bundle": "^1.45", + "symfony/stopwatch": "6.1.*", + "symfony/web-profiler-bundle": "6.1.*" } } diff --git a/www/composer.lock b/www/composer.lock index 3aded40..a9e8280 100644 --- a/www/composer.lock +++ b/www/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5ee7614277dd908e6485a36c9b423ace", + "content-hash": "3c15648c7236906eca7be41b43e18dcf", "packages": [ { "name": "doctrine/annotations", @@ -1373,6 +1373,74 @@ }, "time": "2022-05-23T21:33:49+00:00" }, + { + "name": "egulias/email-validator", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/f88dcf4b14af14a98ad96b14b2b317969eab6715", + "reference": "f88dcf4b14af14a98ad96b14b2b317969eab6715", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.2", + "php": ">=7.2", + "symfony/polyfill-intl-idn": "^1.15" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5.8|^9.3.3", + "vimeo/psalm": "^4" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2022-06-18T20:57:19+00:00" + }, { "name": "friendsofphp/proxy-manager-lts", "version": "v1.0.12", @@ -2988,6 +3056,108 @@ ], "time": "2022-08-07T09:39:47+00:00" }, + { + "name": "symfony/form", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "b435d0eebfcd6985e8bdc984e67888278a6e6d6d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/b435d0eebfcd6985e8bdc984e67888278a6e6d6d", + "reference": "b435d0eebfcd6985e8bdc984e67888278a6e6d6d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/options-resolver": "^5.4|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "phpunit/phpunit": "<5.4.3", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/doctrine-bridge": "<5.4", + "symfony/error-handler": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<1.1.7", + "symfony/twig-bridge": "<5.4" + }, + "require-dev": { + "doctrine/collections": "~1.0", + "symfony/config": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/html-sanitizer": "^6.1", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "symfony/security-csrf": "For protecting forms against CSRF attacks.", + "symfony/twig-bridge": "For templating with Twig.", + "symfony/validator": "For form validation." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Form\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows to easily create, process and reuse HTML forms", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/form/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T15:00:40+00:00" + }, { "name": "symfony/framework-bundle", "version": "v6.1.3", @@ -3401,6 +3571,300 @@ ], "time": "2022-04-12T16:22:53+00:00" }, + { + "name": "symfony/mailer", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b2db228a93278863d1567f90d7caf26922dbfede" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b2db228a93278863d1567f90d7caf26922dbfede", + "reference": "b2db228a93278863d1567f90d7caf26922dbfede", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<5.4" + }, + "require-dev": { + "symfony/http-client-contracts": "^1.1|^2|^3", + "symfony/messenger": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-27T15:50:51+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "9c0247994fc6584da8591ba64b2bffaace9df87d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/9c0247994fc6584da8591ba64b2bffaace9df87d", + "reference": "9c0247994fc6584da8591ba64b2bffaace9df87d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T13:46:29+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a3016f5442e28386ded73c43a32a5b68586dd1c4", + "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-25T11:15:52+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "264894821636b77bb8282db6ec33b8b07b7a0678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/264894821636b77bb8282db6ec33b8b07b7a0678", + "reference": "264894821636b77bb8282db6ec33b8b07b7a0678", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/security-core": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T14:45:06+00:00" + }, { "name": "symfony/polyfill-intl-grapheme", "version": "v1.26.0", @@ -3482,6 +3946,180 @@ ], "time": "2022-05-24T11:49:31+00:00" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "e407643d610e5f2c8a4b14189150f68934bf5e48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e407643d610e5f2c8a4b14189150f68934bf5e48", + "reference": "e407643d610e5f2c8a4b14189150f68934bf5e48", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, { "name": "symfony/polyfill-intl-normalizer", "version": "v1.26.0", @@ -3649,6 +4287,174 @@ ], "time": "2022-05-24T11:49:31+00:00" }, + { + "name": "symfony/property-access", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "25108ee9b62d6ef0815007d9c7cf6a7ba40bb7c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/25108ee9b62d6ef0815007d9c7cf6a7ba40bb7c5", + "reference": "25108ee9b62d6ef0815007d9c7cf6a7ba40bb7c5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/property-info": "^5.4|^6.0" + }, + "require-dev": { + "symfony/cache": "^5.4|^6.0" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-27T17:24:16+00:00" + }, + { + "name": "symfony/property-info", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "2fc363ed2f2b5d3b231ed0824e066d140d3fd1d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/2fc363ed2f2b5d3b231ed0824e066d140d3fd1d8", + "reference": "2fc363ed2f2b5d3b231ed0824e066d140d3fd1d8", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0", + "symfony/cache": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "To use the PHPDoc", + "psr/cache-implementation": "To cache results", + "symfony/doctrine-bridge": "To use Doctrine metadata", + "symfony/serializer": "To use Serializer metadata" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-19T08:34:05+00:00" + }, { "name": "symfony/proxy-manager-bridge", "version": "v6.1.0", @@ -3879,6 +4685,351 @@ ], "time": "2022-06-27T17:24:16+00:00" }, + { + "name": "symfony/security-bundle", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "1410129e36e5d0cf4bde73f4ed5d9e18acff06b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/1410129e36e5d0cf4bde73f4ed5d9e18acff06b3", + "reference": "1410129e36e5d0cf4bde73f4ed5d9e18acff06b3", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.1", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/password-hasher": "^5.4|^6.0", + "symfony/security-core": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/security-http": "^5.4|^6.0" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/console": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/ldap": "<5.4", + "symfony/twig-bundle": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "symfony/asset": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/ldap": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/twig-bridge": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T13:46:29+00:00" + }, + { + "name": "symfony/security-core", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "6c3b79685d7546adf72898b3dc4fa6eb6b488c42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/6c3b79685d7546adf72898b3dc4fa6eb6b488c42", + "reference": "6c3b79685d7546adf72898b3dc4fa6eb6b488c42", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^1.1|^2|^3", + "symfony/password-hasher": "^5.4|^6.0", + "symfony/service-contracts": "^1.1.6|^2|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/ldap": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/validator": "<5.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/ldap": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0" + }, + "suggest": { + "psr/container-implementation": "To instantiate the Security class", + "symfony/event-dispatcher": "", + "symfony/expression-language": "For using the expression voter", + "symfony/http-foundation": "", + "symfony/ldap": "For using LDAP integration", + "symfony/validator": "For using the user password constraint" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T13:46:29+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "b44d74295a5651298de8c2760ba50bef3b97f34b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/b44d74295a5651298de8c2760ba50bef3b97f34b", + "reference": "b44d74295a5651298de8c2760ba50bef3b97f34b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/security-core": "^5.4|^6.0" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "require-dev": { + "symfony/http-foundation": "^5.4|^6.0" + }, + "suggest": { + "symfony/http-foundation": "For using the class SessionTokenStorage." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-14T12:53:54+00:00" + }, + { + "name": "symfony/security-http", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "941b4e6628214711874606d5f80510b556a21622" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/941b4e6628214711874606d5f80510b556a21622", + "reference": "941b4e6628214711874606d5f80510b556a21622", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^6.1", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/security-core": "^5.4.7|^6.0" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4.9|>=6,<6.0.9", + "symfony/security-bundle": "<5.4", + "symfony/security-csrf": "<5.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0" + }, + "suggest": { + "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs", + "symfony/security-csrf": "For using tokens to protect authentication/logout attempts" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-29T07:42:06+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.1.1", @@ -4496,6 +5647,114 @@ ], "time": "2022-05-27T16:55:36+00:00" }, + { + "name": "symfony/validator", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "14a14730609ad6c4f82153ddebf263800833b1cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/14a14730609ad6c4f82153ddebf263800833b1cc", + "reference": "14a14730609ad6c4f82153ddebf263800833b1cc", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^1.1|^2|^3" + }, + "conflict": { + "doctrine/annotations": "<1.13", + "doctrine/lexer": "<1.1", + "phpunit/phpunit": "<5.4.3", + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13", + "egulias/email-validator": "^2.1.10|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "egulias/email-validator": "Strict (RFC compliant) email validation", + "psr/cache-implementation": "For using the mapping cache.", + "symfony/config": "", + "symfony/expression-language": "For using the Expression validator and the ExpressionLanguageSyntax constraints", + "symfony/http-foundation": "", + "symfony/intl": "", + "symfony/property-access": "For accessing properties within comparison constraints", + "symfony/property-info": "To automatically add NotNull and Type constraints", + "symfony/translation": "For translating validation errors.", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T13:46:29+00:00" + }, { "name": "symfony/var-dumper", "version": "v6.1.3", @@ -4803,6 +6062,55 @@ ], "time": "2022-07-20T14:45:06+00:00" }, + { + "name": "symfonycasts/verify-email-bundle", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/SymfonyCasts/verify-email-bundle.git", + "reference": "725b33d1cdeda35055d3dee61f0c84c76f6da9ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SymfonyCasts/verify-email-bundle/zipball/725b33d1cdeda35055d3dee61f0c84c76f6da9ae", + "reference": "725b33d1cdeda35055d3dee61f0c84c76f6da9ae", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/config": "^4.4 | ^5.0 | ^6.0", + "symfony/dependency-injection": "^4.4 | ^5.0 | ^6.0", + "symfony/deprecation-contracts": "^2.2 | ^3.0", + "symfony/http-kernel": "^4.4 | ^5.0 | ^6.0", + "symfony/routing": "^4.4 | ^5.0 | ^6.0" + }, + "conflict": { + "symfony/framework-bundle": "<4.4" + }, + "require-dev": { + "doctrine/orm": "^2.7", + "doctrine/persistence": "^2.0", + "symfony/framework-bundle": "^4.4 | ^5.0 | ^6.0", + "symfony/phpunit-bridge": "^5.0 | ^6.0", + "vimeo/psalm": "^4.3" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "SymfonyCasts\\Bundle\\VerifyEmail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple, stylish Email Verification for Symfony", + "support": { + "issues": "https://github.com/SymfonyCasts/verify-email-bundle/issues", + "source": "https://github.com/SymfonyCasts/verify-email-bundle/tree/v1.11.0" + }, + "time": "2022-07-12T16:51:29+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.4.0", @@ -5249,6 +6557,84 @@ } ], "time": "2022-07-26T12:31:45+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v6.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "6589c2ee4b94d7df2f8ca160ec41265fee3f33eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/6589c2ee4b94d7df2f8ca160ec41265fee3f33eb", + "reference": "6589c2ee4b94d7df2f8ca160ec41265fee3f33eb", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/http-kernel": "^6.1", + "symfony/routing": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "symfony/form": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4" + }, + "require-dev": { + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-12T09:53:37+00:00" } ], "aliases": [], diff --git a/www/config/bundles.php b/www/config/bundles.php index e91c854..8890d6d 100644 --- a/www/config/bundles.php +++ b/www/config/bundles.php @@ -9,4 +9,7 @@ return [ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], ]; diff --git a/www/config/packages/doctrine.yaml b/www/config/packages/doctrine.yaml index 8fe1dd4..3215a8d 100644 --- a/www/config/packages/doctrine.yaml +++ b/www/config/packages/doctrine.yaml @@ -1,6 +1,15 @@ doctrine: dbal: - url: '%env(resolve:DATABASE_URL)%' + default_connection: default + connections: + default: + dbname: birdnet_default_db + url: '%env(resolve:DATABASE_DEFAULT_URL)%' + # wrapper_class: AppBundle\Connections\ConnectionDefault + observations: + dbname: birdnet_observations_db + url: '%env(resolve:DATABASE_OBSERVATIONS_URL)%' + wrapper_class: App\AppBundle\Connections\ConnectionObservations # IMPORTANT: You MUST configure your server version, # either here or in the DATABASE_URL env var (see .env file) diff --git a/www/config/packages/mailer.yaml b/www/config/packages/mailer.yaml new file mode 100644 index 0000000..56a650d --- /dev/null +++ b/www/config/packages/mailer.yaml @@ -0,0 +1,3 @@ +framework: + mailer: + dsn: '%env(MAILER_DSN)%' diff --git a/www/config/packages/security.yaml b/www/config/packages/security.yaml new file mode 100644 index 0000000..663fbf5 --- /dev/null +++ b/www/config/packages/security.yaml @@ -0,0 +1,43 @@ +security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: username + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: app_user_provider + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#the-firewall + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } + +when@test: + security: + password_hashers: + # By default, password hashers are resource intensive and take time. This is + # important to generate secure password hashes. In tests however, secure hashes + # are not important, waste resources and increase test times. The following + # reduces the work factor to the lowest possible values. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/www/config/packages/validator.yaml b/www/config/packages/validator.yaml new file mode 100644 index 0000000..0201281 --- /dev/null +++ b/www/config/packages/validator.yaml @@ -0,0 +1,13 @@ +framework: + validation: + email_validation_mode: html5 + + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/www/config/packages/web_profiler.yaml b/www/config/packages/web_profiler.yaml new file mode 100644 index 0000000..b946111 --- /dev/null +++ b/www/config/packages/web_profiler.yaml @@ -0,0 +1,17 @@ +when@dev: + web_profiler: + toolbar: true + intercept_redirects: false + + framework: + profiler: + only_exceptions: false + collect_serializer_data: true + +when@test: + web_profiler: + toolbar: false + intercept_redirects: false + + framework: + profiler: { collect: false } diff --git a/www/config/routes/web_profiler.yaml b/www/config/routes/web_profiler.yaml new file mode 100644 index 0000000..8d85319 --- /dev/null +++ b/www/config/routes/web_profiler.yaml @@ -0,0 +1,8 @@ +when@dev: + web_profiler_wdt: + resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + prefix: /_wdt + + web_profiler_profiler: + resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + prefix: /_profiler diff --git a/www/config/services.yaml b/www/config/services.yaml index a6fd91a..e6bfa1a 100644 --- a/www/config/services.yaml +++ b/www/config/services.yaml @@ -23,5 +23,8 @@ services: - '../src/Entity/' - '../src/Kernel.php' + # AppBundle\Connections\ExtendedConnection\ConnectionDefault: '@doctrine.dbal.default_connection' + App\AppBundle\Connections\ConnectionObservations: '@doctrine.dbal.observations_connection' + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/www/docker-compose.override.yml b/www/docker-compose.override.yml index f2247d5..6104c8b 100644 --- a/www/docker-compose.override.yml +++ b/www/docker-compose.override.yml @@ -6,3 +6,9 @@ services: ports: - "5432" ###< doctrine/doctrine-bundle ### + +###> symfony/mailer ### + mailer: + image: schickling/mailcatcher + ports: [1025, 1080] +###< symfony/mailer ### diff --git a/www/migrations/Version20220818163807.php b/www/migrations/Version20220818163807.php new file mode 100644 index 0000000..b8b01d1 --- /dev/null +++ b/www/migrations/Version20220818163807.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json) + , password VARCHAR(255) NOT NULL)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649F85E0677 ON user (username)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE user'); + } +} diff --git a/www/migrations/Version20220818165607.php b/www/migrations/Version20220818165607.php new file mode 100644 index 0000000..4089340 --- /dev/null +++ b/www/migrations/Version20220818165607.php @@ -0,0 +1,44 @@ +addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, username, roles, password FROM user'); + $this->addSql('DROP TABLE user'); + $this->addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json) + , password VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL)'); + $this->addSql('INSERT INTO user (id, username, roles, password) SELECT id, username, roles, password FROM __temp__user'); + $this->addSql('DROP TABLE __temp__user'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649F85E0677 ON user (username)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON user (email)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, username, roles, password FROM user'); + $this->addSql('DROP TABLE user'); + $this->addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json) + , password VARCHAR(255) NOT NULL)'); + $this->addSql('INSERT INTO user (id, username, roles, password) SELECT id, username, roles, password FROM __temp__user'); + $this->addSql('DROP TABLE __temp__user'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649F85E0677 ON user (username)'); + } +} diff --git a/www/src/AppBundle/Connections.php b/www/src/AppBundle/Connections.php new file mode 100644 index 0000000..07b16e3 --- /dev/null +++ b/www/src/AppBundle/Connections.php @@ -0,0 +1,10 @@ +}/auth", name="auth_i18n") */ - public function index(Connection $connection) + public function index() { return $this->redirectToRoute("login"); } - + /** * @Route("/auth/login", name="login") * @Route("/{_locale<%app.supported_locales%>}/auth/login", name="login_i18n") @@ -30,4 +29,17 @@ class AuthController extends AbstractController ]); } + + /** + * @Route("/auth/register", name="register") + * @Route("/{_locale<%app.supported_locales%>}/auth/register", name="register_i18n") + */ + public function register(UserPasswordHasherInterface $passwordHasher) + { + $user = new User(); + $plaintextPassword = ""; + $hashedPassword = $passwordHasher->hashPassword($user, $plaintextPassword); + $user->setPassword($hashedPassword); + } + } \ No newline at end of file diff --git a/www/src/Controller/HomeController.php b/www/src/Controller/HomeController.php index ccf9c63..074272a 100644 --- a/www/src/Controller/HomeController.php +++ b/www/src/Controller/HomeController.php @@ -6,18 +6,23 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Doctrine\DBAL\Connection; +use App\AppBundle\Connections\ConnectionObservations; class HomeController extends AbstractController { private Connection $connection; + public function __construct(ConnectionObservations $connection) + { + $this->connection = $connection; + } + /** * @Route("", name="home") * @Route("/{_locale<%app.supported_locales%>}/", name="home_i18n") */ - public function index(Connection $connection) + public function index() { - $this->connection = $connection; return $this->render('index.html.twig', [ "stats" => $this->get_stats(), "charts" => $this->last_chart_generated(), diff --git a/www/src/Controller/RecordsController.php b/www/src/Controller/RecordsController.php index d6827c8..90385f6 100644 --- a/www/src/Controller/RecordsController.php +++ b/www/src/Controller/RecordsController.php @@ -6,12 +6,17 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Doctrine\DBAL\Connection; +use App\AppBundle\Connections\ConnectionObservations; class RecordsController extends AbstractController { - private Connection $connection; + private ConnectionObservations $connection; + public function __construct(ConnectionObservations $connection) + { + $this->connection = $connection; + } + /** * @Route("/records", name="records") * @Route("/{_locale<%app.supported_locales%>}/records/{date}", name="records_i18n") diff --git a/www/src/Controller/RegistrationController.php b/www/src/Controller/RegistrationController.php new file mode 100644 index 0000000..94bf0dc --- /dev/null +++ b/www/src/Controller/RegistrationController.php @@ -0,0 +1,90 @@ +emailVerifier = $emailVerifier; + } + + /** + * @Route("/auth/register", name="register") + * @Route("/{_locale<%app.supported_locales%>}/auth/register", name="register_i18n") + */ + public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response + { + $user = new User(); + $form = $this->createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // encode the plain password + $user->setPassword( + $userPasswordHasher->hashPassword( + $user, + $form->get('plainPassword')->getData() + ) + ); + + $entityManager->persist($user); + $entityManager->flush(); + + // generate a signed url and email it to the user + $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user, + (new TemplatedEmail()) + ->from(new Address('mailer@1ib.re', 'BirdNET-stream Mail Bot')) + ->to($user->getEmail()) + ->subject('Please Confirm your Email') + ->htmlTemplate('registration/confirmation_email.html.twig') + ); + // do anything else you need here, like send an email + + return $this->redirectToRoute('login_i18n'); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form->createView(), + ]); + } + + /** + * @Route("/auth/verify/email", name="verify_email") + * @Route("/{_locale<%app.supported_locales%>}/auth/verify/email", name="verify_email_i18n") + */ + public function verifyUserEmail(Request $request, TranslatorInterface $translator): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + // validate email confirmation link, sets User::isVerified=true and persists + try { + $this->emailVerifier->handleEmailConfirmation($request, $this->getUser()); + } catch (VerifyEmailExceptionInterface $exception) { + $this->addFlash('verify_email_error', $translator->trans($exception->getReason(), [], 'VerifyEmailBundle')); + + return $this->redirectToRoute('register_i18n'); + } + + // @TODO Change the redirect on success and handle or remove the flash message in your templates + $this->addFlash('success', 'Your email address has been verified.'); + + return $this->redirectToRoute('login_i18n'); + } +} \ No newline at end of file diff --git a/www/src/Controller/ServicesController.php b/www/src/Controller/ServicesController.php index a1bc070..7d37814 100644 --- a/www/src/Controller/ServicesController.php +++ b/www/src/Controller/ServicesController.php @@ -15,7 +15,7 @@ class ServicesController extends AbstractController /** * @Route("/services/status", name="services_status") - * @Route("/{_locale<%app.supported_locales%>}/services/status", name="service_status_i18n") + * @Route("/{_locale<%app.supported_locales%>}/services/status", name="services_status_i18n") */ public function service_status() { $status = array_map(function($service) { diff --git a/www/src/Controller/StatsController.php b/www/src/Controller/StatsController.php index a3d84af..32f5e9c 100644 --- a/www/src/Controller/StatsController.php +++ b/www/src/Controller/StatsController.php @@ -5,17 +5,22 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Doctrine\DBAL\Connection; +use App\AppBundle\Connections\ConnectionObservations; class StatsController extends AbstractController { - private Connection $connection; + private ConnectionObservations $connection; + + public function __construct(ConnectionObservations $connection) + { + $this->connection = $connection; + } /** * @Route("/stats", name="stats") * @Route("/{_locale<%app.supported_locales%>}/stats", name="stats_i18n") */ - public function index(Connection $connection) + public function index() { return $this->render("stats/index.html.twig"); } diff --git a/www/src/Controller/TodayController.php b/www/src/Controller/TodayController.php index 452de59..2d90ec4 100644 --- a/www/src/Controller/TodayController.php +++ b/www/src/Controller/TodayController.php @@ -5,27 +5,30 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Doctrine\DBAL\Connection; +use App\AppBundle\Connections\ConnectionObservations; class TodayController extends AbstractController -{ +{ private ConnectionObservations $connection; - private Connection $connection; + public function __construct(ConnectionObservations $connection) + { + $this->connection = $connection; + } /** * @Route("/today", name="today") * @Route("/{_locale<%app.supported_locales%>}/today", name="today_i18n") */ - public function today(Connection $connection) + public function today(ConnectionObservations $connection) { - return $this->redirectToRoute("today_species"); + return $this->redirectToRoute("today_species_i18n"); } /** * @Route("/today/species", name="today_species") * @Route("/{_locale<%app.supported_locales%>}/today/species", name="today_species_i18n") */ - public function today_species_page(Connection $connection) + public function today_species_page(ConnectionObservations $connection) { $this->connection = $connection; $date = date('Y-m-d'); @@ -39,9 +42,8 @@ class TodayController extends AbstractController * @Route("/today/species/{id}", name="today_species_id") * @Route("/{_locale<%app.supported_locales%>}/today/species/{id}", name="today_species_id_i18n") */ - public function today_species_by_id(Connection $connection, $id) + public function today_species_by_id($id) { - $this->connection = $connection; $date = date('Y-m-d'); return $this->render('today/species.html.twig', [ "date" => $date, @@ -54,7 +56,7 @@ class TodayController extends AbstractController * @Route("/today/{date}", name="today_date") * @Route("/{_locale<%app.supported_locales%>}/today/{date}", name="today_date_i18n") */ - public function today_date(Connection $connection, $date) + public function today_date($date="2022-08-13") { return $this->redirectToRoute('today_species_date', array('date' => $date)); } @@ -63,9 +65,8 @@ class TodayController extends AbstractController * @Route("/today/{date}/species", name="today_species_date") * @Route("/{_locale<%app.supported_locales%>}/today/{date}/species", name="today_species_date_i18n") */ - public function today_species_by_date(Connection $connection, $date) + public function today_species_by_date($date="2022-08-13") { - $this->connection = $connection; return $this->render('today/index.html.twig', [ "date" => $date, "results" => $this->recorded_species_by_date($date) @@ -76,9 +77,8 @@ class TodayController extends AbstractController * @Route("/today/{date}/species/{id}", name="today_species_id_and_date") * @Route("/{_locale<%app.supported_locales%>}/today/{date}/species/{id}", name="today_species_id_and_date_i18n") */ - public function today_species_by_id_and_date(Connection $connection, $date, $id) + public function today_species_by_id_and_date($id, $date="2022-08-13") { - $this->connection = $connection; return $this->render('today/species.html.twig', [ "date" => $date, "results" => $this->recorded_species_by_id_and_date($id, $date) diff --git a/www/src/Entity/User.php b/www/src/Entity/User.php new file mode 100644 index 0000000..dedde72 --- /dev/null +++ b/www/src/Entity/User.php @@ -0,0 +1,131 @@ +id; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): self + { + $this->username = $username; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->username; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * @see UserInterface + */ + public function eraseCredentials() + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + public function isVerified(): bool + { + return $this->isVerified; + } + + public function setIsVerified(bool $isVerified): self + { + $this->isVerified = $isVerified; + + return $this; + } +} diff --git a/www/src/Form/RegistrationFormType.php b/www/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..f661644 --- /dev/null +++ b/www/src/Form/RegistrationFormType.php @@ -0,0 +1,55 @@ +add('username') + ->add('agreeTerms', CheckboxType::class, [ + 'mapped' => false, + 'constraints' => [ + new IsTrue([ + 'message' => 'You should agree to our terms.', + ]), + ], + ]) + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/www/src/Repository/UserRepository.php b/www/src/Repository/UserRepository.php new file mode 100644 index 0000000..6689647 --- /dev/null +++ b/www/src/Repository/UserRepository.php @@ -0,0 +1,83 @@ + + * + * @method User|null find($id, $lockMode = null, $lockVersion = null) + * @method User|null findOneBy(array $criteria, array $orderBy = null) + * @method User[] findAll() + * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + public function add(User $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(User $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + } + + $user->setPassword($newHashedPassword); + + $this->add($user, true); + } + +// /** +// * @return User[] Returns an array of User objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('u.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?User +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/www/src/Security/EmailVerifier.php b/www/src/Security/EmailVerifier.php new file mode 100644 index 0000000..6677cfa --- /dev/null +++ b/www/src/Security/EmailVerifier.php @@ -0,0 +1,52 @@ +verifyEmailHelper->generateSignature( + $verifyEmailRouteName, + $user->getId(), + $user->getEmail() + ); + + $context = $email->getContext(); + $context['signedUrl'] = $signatureComponents->getSignedUrl(); + $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey(); + $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData(); + + $email->context($context); + + $this->mailer->send($email); + } + + /** + * @throws VerifyEmailExceptionInterface + */ + public function handleEmailConfirmation(Request $request, UserInterface $user): void + { + $this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getEmail()); + + $user->setIsVerified(true); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + } +} diff --git a/www/symfony.lock b/www/symfony.lock index 13ec260..d8bd1dd 100644 --- a/www/symfony.lock +++ b/www/symfony.lock @@ -90,6 +90,18 @@ "src/Kernel.php" ] }, + "symfony/mailer": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.3", + "ref": "97a61eabb351d7f6cb7702039bcfe07fe9d7e03c" + }, + "files": [ + "config/packages/mailer.yaml" + ] + }, "symfony/maker-bundle": { "version": "1.45", "recipe": { @@ -112,6 +124,18 @@ "config/routes.yaml" ] }, + "symfony/security-bundle": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "8a5b112826f7d3d5b07027f93786ae11a1c7de48" + }, + "files": [ + "config/packages/security.yaml" + ] + }, "symfony/translation": { "version": "6.1", "recipe": { @@ -138,6 +162,31 @@ "templates/base.html.twig" ] }, + "symfony/validator": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" + }, + "files": [ + "config/packages/validator.yaml" + ] + }, + "symfony/web-profiler-bundle": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "e42b3f0177df239add25373083a564e5ead4e13a" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] + }, "symfony/webpack-encore-bundle": { "version": "1.15", "recipe": { @@ -157,6 +206,9 @@ "webpack.config.js" ] }, + "symfonycasts/verify-email-bundle": { + "version": "v1.11.0" + }, "twig/extra-bundle": { "version": "v3.4.0" } diff --git a/www/templates/menu.html.twig b/www/templates/menu.html.twig index 9b6c0f0..375d41a 100644 --- a/www/templates/menu.html.twig +++ b/www/templates/menu.html.twig @@ -8,51 +8,42 @@ diff --git a/www/templates/registration/confirmation_email.html.twig b/www/templates/registration/confirmation_email.html.twig new file mode 100644 index 0000000..bc307f2 --- /dev/null +++ b/www/templates/registration/confirmation_email.html.twig @@ -0,0 +1,11 @@ +

Hi! Please confirm your email!

+ +

+ Please confirm your email address by clicking the following link:

+ Confirm my Email. + This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}. +

+ +

+ Cheers! +

diff --git a/www/templates/registration/register.html.twig b/www/templates/registration/register.html.twig new file mode 100644 index 0000000..4720c77 --- /dev/null +++ b/www/templates/registration/register.html.twig @@ -0,0 +1,21 @@ +{% extends 'base.html.twig' %} + +{% block title %}Register{% endblock %} + +{% block body %} + {% for flash_error in app.flashes('verify_email_error') %} + + {% endfor %} + +

Register

+ + {{ form_start(registrationForm) }} + {{ form_row(registrationForm.username) }} + {{ form_row(registrationForm.plainPassword, { + label: 'Password' + }) }} + {{ form_row(registrationForm.agreeTerms) }} + + + {{ form_end(registrationForm) }} +{% endblock %} diff --git a/www/templates/utils/locale-switcher.html.twig b/www/templates/utils/locale-switcher.html.twig index 2c9acd3..fa6ca2c 100644 --- a/www/templates/utils/locale-switcher.html.twig +++ b/www/templates/utils/locale-switcher.html.twig @@ -3,10 +3,7 @@ {{ name }} diff --git a/www/templates/utils/nav-item.html.twig b/www/templates/utils/nav-item.html.twig index 6c3d539..9b89bfe 100644 --- a/www/templates/utils/nav-item.html.twig +++ b/www/templates/utils/nav-item.html.twig @@ -1,6 +1,6 @@
  • + href="{{ path(route ~ "_i18n", { _locale: app.request.locale }) }}"> {{ text }}