diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 2f1fa24..987d71f 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -7,8 +7,8 @@ xdebug_enabled: false additional_hostnames: [] additional_fqdns: [] database: - type: mariadb - version: "10.11" + type: postgres + version: "17" use_dns_when_possible: true composer_version: "2" web_environment: [] diff --git a/.env b/.env index f07c5f4..691923c 100644 --- a/.env +++ b/.env @@ -28,3 +28,7 @@ APP_SECRET= # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" ###< doctrine/doctrine-bundle ### + +###> symfony/mailer ### +MAILER_DSN=null://null +###< symfony/mailer ### diff --git a/composer.json b/composer.json index ce0b41a..3a15cbb 100644 --- a/composer.json +++ b/composer.json @@ -14,11 +14,15 @@ "symfony/console": "7.2.*", "symfony/dotenv": "7.2.*", "symfony/flex": "^2", + "symfony/form": "^7.2", "symfony/framework-bundle": "7.2.*", + "symfony/mailer": "7.2.*", "symfony/runtime": "7.2.*", "symfony/security-bundle": "7.2.*", "symfony/twig-bundle": "7.2.*", + "symfony/validator": "^7.2", "symfony/yaml": "7.2.*", + "symfonycasts/verify-email-bundle": "^1.17", "twig/extra-bundle": "^2.12|^3.0", "twig/twig": "^2.12|^3.0" }, diff --git a/composer.lock b/composer.lock index c8dd9b8..52b99da 100644 --- a/composer.lock +++ b/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": "7837d9357389519c386e3d8b7d9f3690", + "content-hash": "23e89c425db358cc205ae7de6f407428", "packages": [ { "name": "doctrine/cache", @@ -1229,6 +1229,73 @@ }, "time": "2024-10-21T18:21:57+00:00" }, + { + "name": "egulias/email-validator", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "b115554301161fa21467629f1e1391c1936de517" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", + "reference": "b115554301161fa21467629f1e1391c1936de517", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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/4.0.3" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2024-12-27T00:36:43+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -2654,6 +2721,103 @@ ], "time": "2024-10-07T08:51:54+00:00" }, + { + "name": "symfony/form", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "264cff30f52f12149aff92bbc23e78160a45c2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/264cff30f52f12149aff92bbc23e78160a45c2f3", + "reference": "264cff30f52f12149aff92bbc23e78160a45c2f3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/options-resolver": "^6.4|^7.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/error-handler": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "doctrine/collections": "^1.0|^2.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/html-sanitizer": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "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/v7.2.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": "2024-11-27T11:55:00+00:00" + }, { "name": "symfony/framework-bundle", "version": "v7.2.2", @@ -2996,6 +3160,237 @@ ], "time": "2024-12-31T14:59:40+00:00" }, + { + "name": "symfony/mailer", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/e4d358702fb66e4c8a2af08e90e7271a62de39cc", + "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.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/v7.2.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": "2024-11-25T15:21:05+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283", + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "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": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "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/v7.2.1" + }, + "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": "2024-12-07T08:50:44+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^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/v7.2.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": "2024-11-20T11:17:29+00:00" + }, { "name": "symfony/password-hasher", "version": "v7.2.0", @@ -3146,6 +3541,173 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78", + "reference": "d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "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.31.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": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "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.31.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": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/polyfill-intl-normalizer", "version": "v1.31.0", @@ -4632,6 +5194,103 @@ ], "time": "2024-12-20T13:38:37+00:00" }, + { + "name": "symfony/validator", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "5c01f00fed258a987ef35f0fefcc069f84111cb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/5c01f00fed258a987ef35f0fefcc069f84111cb4", + "reference": "5c01f00fed258a987ef35f0fefcc069f84111cb4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<7.0", + "symfony/expression-language": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<6.4", + "symfony/property-info": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/type-info": "^7.1", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "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/v7.2.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": "2024-12-30T18:35:15+00:00" + }, { "name": "symfony/var-dumper", "version": "v7.2.0", @@ -4863,6 +5522,52 @@ ], "time": "2024-10-23T06:56:12+00:00" }, + { + "name": "symfonycasts/verify-email-bundle", + "version": "v1.17.3", + "source": { + "type": "git", + "url": "https://github.com/SymfonyCasts/verify-email-bundle.git", + "reference": "2cb1cd94ca7a65471563a5cb91ddf40e8433844e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SymfonyCasts/verify-email-bundle/zipball/2cb1cd94ca7a65471563a5cb91ddf40e8433844e", + "reference": "2cb1cd94ca7a65471563a5cb91ddf40e8433844e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "symfony/config": "^5.4 | ^6.0 | ^7.0", + "symfony/dependency-injection": "^5.4 | ^6.0 | ^7.0", + "symfony/deprecation-contracts": "^2.2 | ^3.0", + "symfony/http-kernel": "^5.4 | ^6.0 | ^7.0", + "symfony/routing": "^5.4 | ^6.0 | ^7.0" + }, + "require-dev": { + "doctrine/orm": "^2.7", + "doctrine/persistence": "^2.0", + "symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0", + "symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0" + }, + "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.17.3" + }, + "time": "2024-12-09T18:44:25+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.18.0", diff --git a/config/bundles.php b/config/bundles.php index 919d771..610213d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -9,4 +9,5 @@ return [ Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], ]; diff --git a/config/packages/csrf.yaml b/config/packages/csrf.yaml new file mode 100644 index 0000000..40d4040 --- /dev/null +++ b/config/packages/csrf.yaml @@ -0,0 +1,11 @@ +# Enable stateless CSRF protection for forms and logins/logouts +framework: + form: + csrf_protection: + token_id: submit + + csrf_protection: + stateless_token_ids: + - submit + - authenticate + - logout diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml new file mode 100644 index 0000000..56a650d --- /dev/null +++ b/config/packages/mailer.yaml @@ -0,0 +1,3 @@ +framework: + mailer: + dsn: '%env(MAILER_DSN)%' diff --git a/config/packages/security.yaml b/config/packages/security.yaml index be3d80c..61597d1 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -35,7 +35,9 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/login, roles: PUBLIC_ACCESS } - - { path: ^/, roles: ROLE_USER } + - { path: ^/register, roles: PUBLIC_ACCESS } + - { path: ^/verify/email, roles: PUBLIC_ACCESS } + - { path: ^/, roles: ROLE_EMAIL_VERIFIED } # - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } diff --git a/config/packages/validator.yaml b/config/packages/validator.yaml new file mode 100644 index 0000000..dd47a6a --- /dev/null +++ b/config/packages/validator.yaml @@ -0,0 +1,11 @@ +framework: + validation: + # 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/migrations/Version20250111183301.php b/migrations/Version20250111183301.php new file mode 100644 index 0000000..0c6fe2c --- /dev/null +++ b/migrations/Version20250111183301.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE "user" ADD is_verified BOOLEAN NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE "user" DROP is_verified'); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/Security/LoginController.php similarity index 91% rename from src/Controller/SecurityController.php rename to src/Controller/Security/LoginController.php index e30e778..761384e 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/Security/LoginController.php @@ -1,6 +1,6 @@ createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->service->register($form->get('plainPassword')->getData(), $user); + + return $this->redirectToRoute('app_verify_email_prompt'); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form, + ]); + } + + #[Route('/register/verify-email', name: 'app_verify_email')] + public function verifyUserEmail(Request $request, UserRepository $userRepository): Response + { + $id = $request->query->get('id'); + if (null === $id) { + return $this->redirectToRoute('app_register'); + } + + if (!$user = $userRepository->findOneById($id)) { + return $this->redirectToRoute('app_register'); + } + + try { + $this->service->handleConfirmation($request, $user); + } catch (VerifyEmailExceptionInterface $exception) { + $this->addFlash('error', $exception->getReason()); + + return $this->redirectToRoute('app_register'); + } + + $this->addFlash('success', 'Deine Email-Adresse wurde bestätigt.'); + + return $this->redirectToRoute('app_home'); + } + + #[Route(path: '/verify/email', name: 'app_verify_email_prompt', methods: Request::METHOD_GET)] + public function verifyEmail(): Response + { + return $this->render('registration/verify-email.html.twig'); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 927dfe8..b9087be 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,14 +2,17 @@ namespace App\Entity; +use App\DataObjects\UserData; use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: '`user`')] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])] +#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] @@ -106,4 +109,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface // If you store any temporary, sensitive data on the user, clear it here // $this->plainPassword = null; } + + public function isVerified(): bool + { + return $this->isVerified; + } + + public function setVerified(bool $isVerified): static + { + $this->isVerified = $isVerified; + + return $this; + } } diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..957e588 --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,55 @@ +add('email') + ->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/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 4f2804e..9528eed 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -11,6 +11,7 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; /** * @extends ServiceEntityRepository + * @method User|null findOneById(int $id) */ class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface { @@ -32,29 +33,4 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader $this->getEntityManager()->persist($user); $this->getEntityManager()->flush(); } - - // /** - // * @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/src/Service/Registration/EmailVerifier.php b/src/Service/Registration/EmailVerifier.php new file mode 100644 index 0000000..0beb36e --- /dev/null +++ b/src/Service/Registration/EmailVerifier.php @@ -0,0 +1,53 @@ +verifyEmailHelper->generateSignature( + $verifyEmailRouteName, + (string) $user->getId(), + (string) $user->getEmail(), + ['id' => $user->getId()] + ); + + $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, User $user): void + { + $this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $user->getId(), (string) $user->getEmail()); + + $user->setRoles(['ROLE_EMAIL_VERIFIED']); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + } +} diff --git a/src/Service/Registration/RegistrationService.php b/src/Service/Registration/RegistrationService.php new file mode 100644 index 0000000..8c40eb1 --- /dev/null +++ b/src/Service/Registration/RegistrationService.php @@ -0,0 +1,54 @@ +setPassword($this->userPasswordHasher->hashPassword($user, $plainPassword)); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user, + (new TemplatedEmail()) + ->from(new Address('noreply@simonis.lol', 'Abiball Registrierung')) + ->to((string) $user->getEmail()) + ->subject('Bitte bestätigen Sie Ihre E-Mail-Adresse') + ->htmlTemplate('registration/confirmation_email.html.twig') + ); + + $this->security->login($user, firewallName: 'main'); + } + + /** + * @throws VerifyEmailExceptionInterface + */ + public function handleConfirmation(Request $request, User $user): void + { + $this->emailVerifier->handleEmailConfirmation($request, $user); + + $user->setRoles(['ROLE_EMAIL_VERIFIED']); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + } +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index 7b67bfa..abc3721 100644 --- a/symfony.lock +++ b/symfony.lock @@ -51,6 +51,18 @@ ".env.dev" ] }, + "symfony/form": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.2", + "ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b" + }, + "files": [ + "config/packages/csrf.yaml" + ] + }, "symfony/framework-bundle": { "version": "7.2", "recipe": { @@ -70,6 +82,18 @@ "src/Kernel.php" ] }, + "symfony/mailer": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.3", + "ref": "09051cfde49476e3c12cd3a0e44289ace1c75a4f" + }, + "files": [ + "config/packages/mailer.yaml" + ] + }, "symfony/maker-bundle": { "version": "1.61", "recipe": { @@ -118,6 +142,18 @@ "templates/base.html.twig" ] }, + "symfony/validator": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd" + }, + "files": [ + "config/packages/validator.yaml" + ] + }, "symfony/web-profiler-bundle": { "version": "7.2", "recipe": { @@ -131,6 +167,9 @@ "config/routes/web_profiler.yaml" ] }, + "symfonycasts/verify-email-bundle": { + "version": "v1.17.3" + }, "twig/extra-bundle": { "version": "v3.18.0" } diff --git a/templates/base.html.twig b/templates/base.html.twig index 1069c14..6871cbe 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -11,6 +11,18 @@ {% endblock %} + {% for flash in app.flashes('error') %} +
+ {{ flash }} +
+ {% endfor %} + + {% for flash in app.flashes('success') %} +
+ {{ flash }} +
+ {% endfor %} + {% block body %}{% endblock %} diff --git a/templates/registration/confirmation_email.html.twig b/templates/registration/confirmation_email.html.twig new file mode 100644 index 0000000..7c79d8a --- /dev/null +++ b/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/templates/registration/register.html.twig b/templates/registration/register.html.twig new file mode 100644 index 0000000..12d32c9 --- /dev/null +++ b/templates/registration/register.html.twig @@ -0,0 +1,23 @@ +{% extends 'base.html.twig' %} + +{% block title %}Register{% endblock %} + +{% block body %} + {% for flash_error in app.flashes('verify_email_error') %} + + {% endfor %} + +

Register

+ + {{ form_errors(registrationForm) }} + + {{ form_start(registrationForm) }} + {{ form_row(registrationForm.email) }} + {{ form_row(registrationForm.plainPassword, { + label: 'Password' + }) }} + {{ form_row(registrationForm.agreeTerms) }} + + + {{ form_end(registrationForm) }} +{% endblock %} diff --git a/templates/registration/verify-email.html.twig b/templates/registration/verify-email.html.twig new file mode 100644 index 0000000..c740cb1 --- /dev/null +++ b/templates/registration/verify-email.html.twig @@ -0,0 +1,11 @@ +{% extends 'base.html.twig' %} + +{% block title %} + Verifiziere deine E-Mail-Adresse +{% endblock %} + +{% block body %} + Verfiziere deine E-Mail-Adresse, um deinen Account zu aktivieren. + Falls du keine E-Mail siehst, überprüfe deinen Spam-Ordner. +{% endblock %} + diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 07b5cd4..fbb4886 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -8,12 +8,6 @@
{{ error.messageKey|trans(error.messageData, 'security') }}
{% endif %} - {% if app.user %} -
- You are logged in as {{ app.user.userIdentifier }}, Logout -
- {% endif %} -

Please sign in

@@ -24,16 +18,6 @@ value="{{ csrf_token('authenticate') }}" > - {# - Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. - See https://symfony.com/doc/current/security/remember_me.html - -
- - -
- #} -