diff --git a/composer.json b/composer.json index 1e8547d3..55e4223a 100644 --- a/composer.json +++ b/composer.json @@ -14,12 +14,15 @@ "doctrine/orm": "^2.13", "sensio/framework-extra-bundle": "^6.2", "symfony/console": "6.1.*", + "symfony/doctrine-bridge": "6.1.*", "symfony/dotenv": "6.1.*", "symfony/flex": "^2", "symfony/framework-bundle": "6.1.*", "symfony/proxy-manager-bridge": "6.1.*", "symfony/runtime": "6.1.*", + "symfony/security-bundle": "6.1.*", "symfony/twig-bundle": "6.1.*", + "symfony/validator": "6.1.*", "symfony/yaml": "6.1.*", "twig/extra-bundle": "^2.12|^3.0", "twig/twig": "^2.12|^3.0" diff --git a/composer.lock b/composer.lock index 1a8c8858..1fe3945d 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": "f2c2af116e47a4800a739f427c3b33aa", + "content-hash": "b063d661313c26c199ccc237b4280fec", "packages": [ { "name": "doctrine/annotations", @@ -2307,16 +2307,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.1.3", + "version": "v6.1.5", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "68b53b14f337dbc6f92cf6f1656a0adad42482e0" + "reference": "ca0fdecd106f81d6bd7f123e77b5830c558e1148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/68b53b14f337dbc6f92cf6f1656a0adad42482e0", - "reference": "68b53b14f337dbc6f92cf6f1656a0adad42482e0", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/ca0fdecd106f81d6bd7f123e77b5830c558e1148", + "reference": "ca0fdecd106f81d6bd7f123e77b5830c558e1148", "shasum": "" }, "require": { @@ -2402,7 +2402,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.1.3" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.1.5" }, "funding": [ { @@ -2418,7 +2418,7 @@ "type": "tidelift" } ], - "time": "2022-07-29T07:42:06+00:00" + "time": "2022-09-08T09:34:40+00:00" }, { "name": "symfony/dotenv", @@ -3254,6 +3254,78 @@ ], "time": "2022-08-26T14:50:30+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", @@ -3502,6 +3574,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", @@ -3733,45 +3973,68 @@ "time": "2022-06-27T17:24:16+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.1.1", + "name": "symfony/security-bundle", + "version": "v6.1.3", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" + "url": "https://github.com/symfony/security-bundle.git", + "reference": "1410129e36e5d0cf4bde73f4ed5d9e18acff06b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", - "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", + "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", - "psr/container": "^2.0" + "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": { - "ext-psr": "<1.1|>=2" - }, - "suggest": { - "symfony/service-implementation": "" + "symfony/browser-kit": "<5.4", + "symfony/console": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/ldap": "<5.4", + "symfony/twig-bundle": "<5.4" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.1-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } + "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\\Contracts\\Service\\": "" + "Symfony\\Bundle\\SecurityBundle\\": "" }, "exclude-from-classmap": [ - "/Test/" + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3780,26 +4043,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.1.1" + "source": "https://github.com/symfony/security-bundle/tree/v6.1.3" }, "funding": [ { @@ -3815,7 +4070,337 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:18:58+00:00" + "time": "2022-07-20T13:46:29+00:00" + }, + { + "name": "symfony/security-core", + "version": "v6.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "a3e6ee1e0bafb22418fb602445631c9d5849055c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/a3e6ee1e0bafb22418fb602445631c9d5849055c", + "reference": "a3e6ee1e0bafb22418fb602445631c9d5849055c", + "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.4" + }, + "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-08-19T14:27:04+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.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "a106f0f55e9942da5aa9181fbf2175512f583449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/a106f0f55e9942da5aa9181fbf2175512f583449", + "reference": "a106f0f55e9942da5aa9181fbf2175512f583449", + "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.4" + }, + "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-08-26T10:32:31+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.1.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": "2022-05-30T19:18:58+00:00" }, { "name": "symfony/stopwatch", @@ -4253,6 +4838,114 @@ ], "time": "2022-05-27T16:55:36+00:00" }, + { + "name": "symfony/validator", + "version": "v6.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "7d7724f550e0f657a591831a7c31e25678ff8779" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/7d7724f550e0f657a591831a7c31e25678ff8779", + "reference": "7d7724f550e0f657a591831a7c31e25678ff8779", + "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.5" + }, + "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-09-17T07:55:45+00:00" + }, { "name": "symfony/var-dumper", "version": "v6.1.3", @@ -6957,7 +7650,9 @@ "platform": { "php": ">=8.1", "ext-ctype": "*", - "ext-iconv": "*" + "ext-iconv": "*", + "ext-pdo": "*", + "ext-zip": "*" }, "platform-dev": [], "plugin-api-version": "2.3.0" diff --git a/config/bundles.php b/config/bundles.php index e170a69a..9e3eed6c 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -9,4 +9,5 @@ Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 00000000..0b66c7b5 --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,42 @@ +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: + app_user_provider: + entity: + class: App\Entity\User + property: email + 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/config/packages/validator.yaml b/config/packages/validator.yaml new file mode 100644 index 00000000..0201281d --- /dev/null +++ b/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/migrations/Version20220930075943AddAuthColumnsToUser.php b/migrations/Version20220930075943AddAuthColumnsToUser.php new file mode 100644 index 00000000..9c2273a4 --- /dev/null +++ b/migrations/Version20220930075943AddAuthColumnsToUser.php @@ -0,0 +1,50 @@ +connection->getDatabasePlatform()->getName(); + if ($dbPlatform === 'postgresql') { + $this->addSql('ALTER TABLE "user" ADD email VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE "user" ADD roles JSON NOT NULL'); + $this->addSql('ALTER TABLE "user" ADD password VARCHAR(255) NOT NULL'); + $this->addSql('CREATE UNIQUE INDEX idx_user_email ON "user" (email)'); + } elseif ($dbPlatform === 'mysql') { + $this->addSql(<<addSql('CREATE UNIQUE INDEX idx_user_email ON user (email)'); + } + } + + public function down(Schema $schema): void + { + $dbPlatform = $this->connection->getDatabasePlatform()->getName(); + if ($dbPlatform === 'postgresql') { + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP INDEX idx_user_email'); + $this->addSql('ALTER TABLE "user" DROP email'); + $this->addSql('ALTER TABLE "user" DROP roles'); + $this->addSql('ALTER TABLE "user" DROP password'); + } elseif ($dbPlatform === 'mysql') { + $this->addSql('DROP INDEX idx_user_email ON `user`'); + $this->addSql('ALTER TABLE `user` DROP email, DROP roles, DROP password'); + } + } +} diff --git a/src/Command/Users/CreateCommand.php b/src/Command/Users/CreateCommand.php new file mode 100644 index 00000000..979d0e40 --- /dev/null +++ b/src/Command/Users/CreateCommand.php @@ -0,0 +1,106 @@ +getManager(); + + $this->entityManager = $entityManager; + $this->passwordHasher = $passwordHasher; + $this->validator = $validator; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addOption('email', '', InputOption::VALUE_OPTIONAL, 'The email of the user.') + ->addOption('password', '', InputOption::VALUE_OPTIONAL, 'The password of the user.') + ; + } + + protected function interact(InputInterface $input, OutputInterface $output): void + { + $helper = $this->getHelper('question'); + + $email = $input->getOption('email'); + if (!$email) { + $question = new Question('Email: '); + $email = $helper->ask($input, $output, $question); + $input->setOption('email', $email); + } + + $password = $input->getOption('password'); + if (!$password) { + $question = new Question('Password (hidden): '); + $question->setHidden(true); + $question->setHiddenFallback(false); + + $password = $helper->ask($input, $output, $question); + $input->setOption('password', $password); + } + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $email = trim($input->getOption('email')); + $password = $input->getOption('password'); + + $user = new Entity\User(); + + $user->setEmail($email); + $hashedPassword = $this->passwordHasher->hashPassword($user, $password); + $user->setPassword($hashedPassword); + $user->setRoles(['ROLE_USER']); + + $errors = $this->validator->validate($user); + if (count($errors) > 0) { + $output = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; + foreach ($errors as $error) { + $output->writeln($error->getMessage()); + } + + return Command::INVALID; + } + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + $output->writeln("The user \"{$user->getEmail()}\" has been created."); + + return Command::SUCCESS; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 38ad03aa..e80f6968 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -4,18 +4,111 @@ use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: '`user`')] -class User +#[UniqueEntity( + fields: 'email', + message: 'The email {{ value }} is already used.', +)] +class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; + #[ORM\Column(length: 255, unique: true)] + #[Assert\NotBlank( + message: 'The email is required.', + )] + #[Assert\Email( + message: 'The email {{ value }} is not a valid email.', + )] + private ?string $email = null; + + /** + * @var string[] + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The hashed password + */ + #[ORM\Column] + private ?string $password = null; + public function getId(): ?int { return $this->id; } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + return $this->roles; + } + + /** + * @param string[] $roles + */ + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string + { + return (string) $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 4e249716..7796f8be 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -5,6 +5,9 @@ use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; /** * @extends ServiceEntityRepository @@ -14,7 +17,7 @@ * @method User[] findAll() * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class UserRepository extends ServiceEntityRepository +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface { public function __construct(ManagerRegistry $registry) { @@ -38,4 +41,18 @@ public function remove(User $entity, bool $flush = false): void $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->getEntityManager()->flush(); + } } diff --git a/symfony.lock b/symfony.lock index 77298c73..a1b7ecbc 100644 --- a/symfony.lock +++ b/symfony.lock @@ -150,6 +150,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/twig-bundle": { "version": "6.1", "recipe": { @@ -163,6 +175,18 @@ "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": { diff --git a/tests/Command/Users/CreateCommandTest.php b/tests/Command/Users/CreateCommandTest.php new file mode 100644 index 00000000..1ea34f19 --- /dev/null +++ b/tests/Command/Users/CreateCommandTest.php @@ -0,0 +1,144 @@ +get('security.user_password_hasher'); + $userRepository = self::getRepository(Entity\User::class); + $email = 'alix@example.com'; + $password = 'secret'; + + $this->assertSame(0, $userRepository->count([])); + + $tester = self::executeCommand('app:users:create', [ + $email, + $password, + ]); + + $this->assertSame(Command::SUCCESS, $tester->getStatusCode()); + $this->assertSame( + "The user \"{$email}\" has been created.\n", + $tester->getDisplay() + ); + $user = $userRepository->findOneBy([]); + $this->assertNotNull($user); + $this->assertSame($email, $user->getEmail()); + $this->assertTrue($passwordHasher->isPasswordValid($user, $password)); + $this->assertSame(['ROLE_USER'], $user->getRoles()); + } + + public function testExecuteWorksWhenPassingOptions(): void + { + /** @var \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface */ + $passwordHasher = self::getContainer()->get('security.user_password_hasher'); + $userRepository = self::getRepository(Entity\User::class); + $email = 'alix@example.com'; + $password = 'secret'; + + $this->assertSame(0, $userRepository->count([])); + + $tester = self::executeCommand('app:users:create', [], [ + '--email' => $email, + '--password' => $password, + ]); + + $this->assertSame(Command::SUCCESS, $tester->getStatusCode()); + $this->assertSame( + "The user \"{$email}\" has been created.\n", + $tester->getDisplay() + ); + $user = $userRepository->findOneBy([]); + $this->assertNotNull($user); + $this->assertSame($email, $user->getEmail()); + $this->assertTrue($passwordHasher->isPasswordValid($user, $password)); + $this->assertSame(['ROLE_USER'], $user->getRoles()); + } + + public function testExecuteFailsIfEmailIsInvalid(): void + { + /** @var \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface */ + $passwordHasher = self::getContainer()->get('security.user_password_hasher'); + $userRepository = self::getRepository(Entity\User::class); + $email = 'alix'; + $password = 'secret'; + + $this->assertSame(0, $userRepository->count([])); + + $tester = self::executeCommand('app:users:create', [], [ + '--email' => $email, + '--password' => $password, + ]); + + $this->assertSame(Command::INVALID, $tester->getStatusCode()); + $this->assertSame( + "The email \"{$email}\" is not a valid email.\n", + $tester->getErrorOutput() + ); + $this->assertSame(0, $userRepository->count([])); + } + + public function testExecuteFailsIfEmailIsEmpty(): void + { + /** @var \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface */ + $passwordHasher = self::getContainer()->get('security.user_password_hasher'); + $userRepository = self::getRepository(Entity\User::class); + $email = ' '; + $password = 'secret'; + + $this->assertSame(0, $userRepository->count([])); + + $tester = self::executeCommand('app:users:create', [], [ + '--email' => $email, + '--password' => $password, + ]); + + $this->assertSame(Command::INVALID, $tester->getStatusCode()); + $this->assertSame( + "The email is required.\n", + $tester->getErrorOutput() + ); + $this->assertSame(0, $userRepository->count([])); + } + + public function testExecuteFailsIfEmailExists(): void + { + /** @var \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface */ + $passwordHasher = self::getContainer()->get('security.user_password_hasher'); + $userRepository = self::getRepository(Entity\User::class); + $email = 'alix@example.com'; + $password = 'secret'; + + $user = new Entity\User(); + $user->setEmail($email); + $user->setPassword(''); + self::$entityManager->persist($user); + self::$entityManager->flush(); + + $this->assertSame(1, $userRepository->count([])); + + $tester = self::executeCommand('app:users:create', [], [ + '--email' => $email, + '--password' => $password, + ]); + + $this->assertSame(Command::INVALID, $tester->getStatusCode()); + $this->assertSame( + "The email \"{$email}\" is already used.\n", + $tester->getErrorOutput() + ); + $this->assertSame(1, $userRepository->count([])); + } +} diff --git a/tests/CommandTestsHelper.php b/tests/CommandTestsHelper.php new file mode 100644 index 00000000..78489671 --- /dev/null +++ b/tests/CommandTestsHelper.php @@ -0,0 +1,42 @@ + $inputs + * @param array $args + */ + protected static function executeCommand( + string $command, + array $inputs = [], + array $args = [], + ): CommandTester { + $command = self::$application->find($command); + $commandTester = new CommandTester($command); + $commandTester->setInputs($inputs); + $commandTester->execute($args, [ + 'capture_stderr_separately' => true, + ]); + return $commandTester; + } +} diff --git a/tests/DatabaseResetterHelper.php b/tests/DatabaseResetterHelper.php new file mode 100644 index 00000000..2fc89ddb --- /dev/null +++ b/tests/DatabaseResetterHelper.php @@ -0,0 +1,43 @@ +boot(); + + /** @var \Doctrine\Persistence\ManagerRegistry */ + $doctrine = $kernel->getContainer()->get('doctrine'); + + /** @var \Doctrine\ORM\EntityManager */ + $entityManager = $doctrine->getManager(); + + $connection = $entityManager->getConnection(); + + $tablesNames = $connection->getSchemaManager()->listTableNames(); + $tablesNames = implode(',', $tablesNames); + + $dbPlatform = $connection->getDatabasePlatform()->getName(); + if ($dbPlatform === 'postgresql') { + $connection->executeStatement(<<executeStatement(<<shutdown(); + } +} diff --git a/tests/EntityManagerHelper.php b/tests/EntityManagerHelper.php new file mode 100644 index 00000000..54d1d34a --- /dev/null +++ b/tests/EntityManagerHelper.php @@ -0,0 +1,49 @@ +boot(); + + /** @var \Doctrine\Persistence\ManagerRegistry */ + $doctrine = $kernel->getContainer()->get('doctrine'); + + /** @var \Doctrine\ORM\EntityManager */ + $entityManager = $doctrine->getManager(); + + self::$entityManager = $entityManager; + + $kernel->shutdown(); + } + + /** + * @afterClass + */ + public static function tearDownEntityManagerHelper(): void + { + self::$entityManager->close(); + self::$entityManager = null; /** @phpstan-ignore-line */ + } + + /** + * @template T of object + * @param class-string $entityName + * @return EntityRepository + */ + public static function getRepository(string $entityName): EntityRepository + { + return self::$entityManager->getRepository($entityName); + } +}