From 11d89c8a10ced4921824211009d28895a789c1b7 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sat, 23 Jun 2018 17:57:10 +0300 Subject: [PATCH 01/24] Separated Entity Managers for magneticod SQLite database and app's PostgreSQL database implemented. Some refactoring has been made. --- config/packages/doctrine.yaml | 43 +++++++++++++------ src/Api/V1/Controller/TorrentController.php | 4 +- src/Controller/MainController.php | 2 +- src/Controller/TorrentController.php | 4 +- src/{ => Magnetico}/Entity/File.php | 4 +- src/{ => Magnetico}/Entity/Torrent.php | 6 +-- .../Repository/TorrentRepository.php | 10 +++-- templates/torrent_list.html.twig | 2 +- templates/torrent_show.html.twig | 4 +- 9 files changed, 51 insertions(+), 28 deletions(-) rename src/{ => Magnetico}/Entity/File.php (91%) rename src/{ => Magnetico}/Entity/Torrent.php (91%) rename src/{ => Magnetico}/Repository/TorrentRepository.php (79%) diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 39536b6..ff0abee 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -7,17 +7,36 @@ parameters: doctrine: dbal: - # configure these for your database server - driver: 'pdo_sqlite' - url: '%env(resolve:DATABASE_URL)%' + default_connection: default + connections: + default: + driver: 'pdo_pgsql' + url: '%env(resolve:APP_DATABASE_URL)%' + magneticod: + driver: 'pdo_sqlite' + url: '%env(resolve:MAGNETICOD_DATABASE_URL)%' orm: auto_generate_proxy_classes: '%kernel.debug%' - naming_strategy: doctrine.orm.naming_strategy.underscore - auto_mapping: true - mappings: - App: - is_bundle: false - type: annotation - dir: '%kernel.project_dir%/src/Entity' - prefix: 'App\Entity' - alias: App + default_entity_manager: default + entity_managers: + default: + connection: default + naming_strategy: doctrine.orm.naming_strategy.underscore + auto_mapping: true + mappings: + App: + is_bundle: false + type: annotation + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App + magneticod: + connection: magneticod + mappings: + Magnetico: + is_bundle: false + type: annotation + dir: '%kernel.project_dir%/src/Magnetico/Entity' + prefix: 'App\Magnetico' + alias: Magnetico + diff --git a/src/Api/V1/Controller/TorrentController.php b/src/Api/V1/Controller/TorrentController.php index a255c98..868f690 100644 --- a/src/Api/V1/Controller/TorrentController.php +++ b/src/Api/V1/Controller/TorrentController.php @@ -4,8 +4,8 @@ namespace App\Api\V1\Controller; use App\Api\V1\DTO\ApiResponse; use App\Api\V1\DTO\ListPage; -use App\Entity\Torrent; -use App\Repository\TorrentRepository; +use App\Magnetico\Entity\Torrent; +use App\Magnetico\Repository\TorrentRepository; use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Pagerfanta; use Symfony\Bundle\FrameworkBundle\Controller\Controller; diff --git a/src/Controller/MainController.php b/src/Controller/MainController.php index 1d22536..e2125a5 100644 --- a/src/Controller/MainController.php +++ b/src/Controller/MainController.php @@ -2,7 +2,7 @@ namespace App\Controller; -use App\Repository\TorrentRepository; +use App\Magnetico\Repository\TorrentRepository; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Controller/TorrentController.php b/src/Controller/TorrentController.php index 737fbed..a6974bd 100644 --- a/src/Controller/TorrentController.php +++ b/src/Controller/TorrentController.php @@ -2,8 +2,8 @@ namespace App\Controller; -use App\Entity\Torrent; -use App\Repository\TorrentRepository; +use App\Magnetico\Entity\Torrent; +use App\Magnetico\Repository\TorrentRepository; use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Pagerfanta; use Symfony\Bundle\FrameworkBundle\Controller\Controller; diff --git a/src/Entity/File.php b/src/Magnetico/Entity/File.php similarity index 91% rename from src/Entity/File.php rename to src/Magnetico/Entity/File.php index 5d04e27..e27477b 100644 --- a/src/Entity/File.php +++ b/src/Magnetico/Entity/File.php @@ -1,6 +1,6 @@ select('COUNT(t.id)') ; - return (int) $qb->getQuery()->getSingleScalarResult(); + try { + return (int) $qb->getQuery()->getSingleScalarResult(); + } catch (\Exception $ex) { + return 0; + } } public function createFindLikeQueryBuilder(string $query): QueryBuilder diff --git a/templates/torrent_list.html.twig b/templates/torrent_list.html.twig index 6973ec5..5133f93 100644 --- a/templates/torrent_list.html.twig +++ b/templates/torrent_list.html.twig @@ -13,7 +13,7 @@ Discovered - {# @var torrent \App\Entity\Torrent #} + {# @var torrent \App\Magnetico\Entity\Torrent #} {% for torrent in torrents %} 🔗 diff --git a/templates/torrent_show.html.twig b/templates/torrent_show.html.twig index 1d8152d..3c49c47 100644 --- a/templates/torrent_show.html.twig +++ b/templates/torrent_show.html.twig @@ -1,7 +1,7 @@ {% extends 'base.html.twig' %} {% block content %} - {# @var torrent \App\Entity\Torrent #} + {# @var torrent \App\Magnetico\Entity\Torrent #} @@ -31,7 +31,7 @@ - {# @var file \App\Entity\File #} + {# @var file \App\Magnetico\Entity\File #} {% for file in torrent.files | sort %} From bd23236efe377c116c863589435716edef1a8ac3 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sat, 23 Jun 2018 18:00:37 +0300 Subject: [PATCH 02/24] composer require security-bundle --- composer.json | 1 + composer.lock | 160 +++++++++++++++++++++++++++++++++- config/bundles.php | 1 + config/packages/security.yaml | 24 +++++ symfony.lock | 12 +++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 config/packages/security.yaml diff --git a/composer.json b/composer.json index ac01ec4..29b7265 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "symfony/lts": "^4@dev", "symfony/monolog-bundle": "^3.3", "symfony/orm-pack": "^1.0", + "symfony/security-bundle": "^4.1", "symfony/serializer-pack": "^1.0", "symfony/translation": "^4.1", "symfony/twig-bundle": "^4.1", diff --git a/composer.lock b/composer.lock index 58e44f8..3c9d7de 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": "16f2a34d0add203515fe657d6dcde23d", + "content-hash": "b6ca92cab0ca13436da7c0a3acde22ad", "packages": [ { "name": "doctrine/annotations", @@ -3163,6 +3163,164 @@ ], "time": "2018-05-30T07:26:09+00:00" }, + { + "name": "symfony/security", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/security.git", + "reference": "2ba804b4af205c060094c28cece0b42a26a67537" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security/zipball/2ba804b4af205c060094c28cece0b42a26a67537", + "reference": "2ba804b4af205c060094c28cece0b42a26a67537", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/http-foundation": "~3.4|~4.0", + "symfony/http-kernel": "~3.4|~4.0", + "symfony/property-access": "~3.4|~4.0" + }, + "replace": { + "symfony/security-core": "self.version", + "symfony/security-csrf": "self.version", + "symfony/security-guard": "self.version", + "symfony/security-http": "self.version" + }, + "require-dev": { + "psr/container": "^1.0", + "psr/log": "~1.0", + "symfony/expression-language": "~3.4|~4.0", + "symfony/finder": "~3.4|~4.0", + "symfony/ldap": "~3.4|~4.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/routing": "~3.4|~4.0", + "symfony/validator": "~3.4|~4.0" + }, + "suggest": { + "psr/container-implementation": "To instantiate the Security class", + "symfony/expression-language": "For using the expression voter", + "symfony/form": "", + "symfony/ldap": "For using the LDAP user and authentication providers", + "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs", + "symfony/validator": "For using the user password constraint" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\": "" + }, + "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", + "homepage": "https://symfony.com", + "time": "2018-05-30T07:26:09+00:00" + }, + { + "name": "symfony/security-bundle", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "8ac1bc3575a40eb57e0ed84f00bcb5964986945b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/8ac1bc3575a40eb57e0ed84f00bcb5964986945b", + "reference": "8ac1bc3575a40eb57e0ed84f00bcb5964986945b", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "php": "^7.1.3", + "symfony/dependency-injection": "^3.4.3|^4.0.3", + "symfony/http-kernel": "^4.1", + "symfony/security": "~4.1" + }, + "conflict": { + "symfony/console": "<3.4", + "symfony/event-dispatcher": "<3.4", + "symfony/framework-bundle": "<=4.1-beta2", + "symfony/security": "4.1.0-beta1|4.1.0-beta2", + "symfony/var-dumper": "<3.4" + }, + "require-dev": { + "doctrine/doctrine-bundle": "~1.5", + "symfony/asset": "~3.4|~4.0", + "symfony/browser-kit": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", + "symfony/css-selector": "~3.4|~4.0", + "symfony/dom-crawler": "~3.4|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/expression-language": "~3.4|~4.0", + "symfony/form": "~3.4|~4.0", + "symfony/framework-bundle": "~4.1", + "symfony/http-foundation": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0", + "symfony/translation": "~3.4|~4.0", + "symfony/twig-bridge": "~3.4|~4.0", + "symfony/twig-bundle": "~3.4|~4.0", + "symfony/validator": "~3.4|~4.0", + "symfony/var-dumper": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0", + "twig/twig": "~1.34|~2.4" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "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": "Symfony SecurityBundle", + "homepage": "https://symfony.com", + "time": "2018-05-25T13:53:35+00:00" + }, { "name": "symfony/serializer", "version": "v4.1.0", diff --git a/config/bundles.php b/config/bundles.php index 00252c1..d8fd2ba 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -11,4 +11,5 @@ return [ WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle::class => ['all' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => 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 0000000..fb4c593 --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,24 @@ +security: + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + in_memory: { memory: ~ } + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + anonymous: true + + # activate different ways to authenticate + + # http_basic: true + # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate + + # form_login: true + # https://symfony.com/doc/current/security/form_login_setup.html + + # 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 } diff --git a/symfony.lock b/symfony.lock index 128f8c9..2aca2c7 100644 --- a/symfony.lock +++ b/symfony.lock @@ -206,6 +206,18 @@ "ref": "cda8b550123383d25827705d05a42acf6819fe4e" } }, + "symfony/security": { + "version": "v4.1.0" + }, + "symfony/security-bundle": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "3.3", + "ref": "f8a63faa0d9521526499c0a8f403c9964ecb0527" + } + }, "symfony/serializer": { "version": "v4.1.0" }, From be63290b818cfbd775093048676f66c709f5b478 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sat, 23 Jun 2018 20:26:52 +0300 Subject: [PATCH 03/24] .env.dist updated according to new database infrastructure. --- .env.dist | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.dist b/.env.dist index f847f27..6bedb2f 100644 --- a/.env.dist +++ b/.env.dist @@ -13,5 +13,6 @@ APP_SECRET=xxx # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # Configure your db driver and server_version in config/packages/doctrine.yaml -DATABASE_URL=sqlite:////home/magnetico/.local/share/magneticod/database.sqlite3 +APP_DATABASE_URL=pdo_pgsql://magnetico_web:dev@127.0.0.1:5432/magnetico_web?application_name=magnetico_web&server_version=9.6 +MAGNETICOD_DATABASE_URL=sqlite:////home/magnetico/.local/share/magneticod/database.sqlite3 ###< doctrine/doctrine-bundle ### From c251c984c575537c197e1dfee397561e1ef32111 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sat, 23 Jun 2018 20:38:54 +0300 Subject: [PATCH 04/24] .env.dist updated according to new database infrastructure. --- .env.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.dist b/.env.dist index 6bedb2f..3c5baaa 100644 --- a/.env.dist +++ b/.env.dist @@ -13,6 +13,6 @@ APP_SECRET=xxx # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # Configure your db driver and server_version in config/packages/doctrine.yaml -APP_DATABASE_URL=pdo_pgsql://magnetico_web:dev@127.0.0.1:5432/magnetico_web?application_name=magnetico_web&server_version=9.6 +APP_DATABASE_URL=pdo_pgsql://$PGUSER:$PGPASSWORD@127.0.0.1:5436/test?application_name=magnetico_web MAGNETICOD_DATABASE_URL=sqlite:////home/magnetico/.local/share/magneticod/database.sqlite3 ###< doctrine/doctrine-bundle ### From a6bc4afc3c3f9c2283fbb4fc0f201ba59036a5f9 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sat, 23 Jun 2018 21:56:03 +0300 Subject: [PATCH 05/24] Test SQLite database included. --- .env.dist | 2 +- tests/database/database.sqlite3 | Bin 0 -> 49152 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/database/database.sqlite3 diff --git a/.env.dist b/.env.dist index 3c5baaa..f256ff0 100644 --- a/.env.dist +++ b/.env.dist @@ -14,5 +14,5 @@ APP_SECRET=xxx # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # Configure your db driver and server_version in config/packages/doctrine.yaml APP_DATABASE_URL=pdo_pgsql://$PGUSER:$PGPASSWORD@127.0.0.1:5436/test?application_name=magnetico_web -MAGNETICOD_DATABASE_URL=sqlite:////home/magnetico/.local/share/magneticod/database.sqlite3 +MAGNETICOD_DATABASE_URL=sqlite:///%kernel.project_dir%/tests/database/database.sqlite3 ###< doctrine/doctrine-bundle ### diff --git a/tests/database/database.sqlite3 b/tests/database/database.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..1a54f0575a402c0c04ab2c96fc111db3b5fc6105 GIT binary patch literal 49152 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 Date: Sat, 23 Jun 2018 23:19:01 +0300 Subject: [PATCH 06/24] Fallback database URLs fixed. --- config/packages/doctrine.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index ff0abee..9d1620c 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -3,7 +3,8 @@ parameters: # This allows you to run cache:warmup even if your # environment variables are not available yet. # You should not need to change this value. - env(DATABASE_URL): '' + env(APP_DATABASE_URL): '' + env(MAGNETICOD_DATABASE_URL): '' doctrine: dbal: From 582def6c2d5aeef0080b2562757ab14941362779 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sun, 24 Jun 2018 00:17:32 +0300 Subject: [PATCH 07/24] PostgreSQL database version. --- config/packages/doctrine.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 9d1620c..26d7f45 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -13,6 +13,7 @@ doctrine: default: driver: 'pdo_pgsql' url: '%env(resolve:APP_DATABASE_URL)%' + #server_version: 9.6 magneticod: driver: 'pdo_sqlite' url: '%env(resolve:MAGNETICOD_DATABASE_URL)%' From 13d001272db78fe6e968abfef831d29509a491f3 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sun, 24 Jun 2018 01:45:18 +0300 Subject: [PATCH 08/24] User and Invite entities added. DTO CreateUserRequest and form type CreateUserRequestType added. --- composer.json | 2 + composer.lock | 356 ++++++++++++++++++++++++++++- src/Entity/Invite.php | 70 ++++++ src/Entity/User.php | 121 ++++++++++ src/Form/CreateUserRequest.php | 39 ++++ src/Form/CreateUserRequestType.php | 31 +++ symfony.lock | 15 ++ 7 files changed, 633 insertions(+), 1 deletion(-) create mode 100644 src/Entity/Invite.php create mode 100644 src/Entity/User.php create mode 100644 src/Form/CreateUserRequest.php create mode 100644 src/Form/CreateUserRequestType.php diff --git a/composer.json b/composer.json index 29b7265..7860ff0 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "sensio/framework-extra-bundle": "^5.1", "symfony/console": "^4.1", "symfony/flex": "^1.0", + "symfony/form": "^4.1", "symfony/framework-bundle": "^4.1", "symfony/lts": "^4@dev", "symfony/monolog-bundle": "^3.3", @@ -26,6 +27,7 @@ "symfony/serializer-pack": "^1.0", "symfony/translation": "^4.1", "symfony/twig-bundle": "^4.1", + "symfony/validator": "^4.1", "symfony/web-server-bundle": "^4.1", "symfony/yaml": "^4.1", "white-october/pagerfanta-bundle": "^1.2" diff --git a/composer.lock b/composer.lock index 3c9d7de..aa6943c 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": "b6ca92cab0ca13436da7c0a3acde22ad", + "content-hash": "0b2aebb419be98d3475cfe76e436f2b0", "packages": [ { "name": "doctrine/annotations", @@ -2270,6 +2270,87 @@ ], "time": "2018-05-02T19:08:56+00:00" }, + { + "name": "symfony/form", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "95f8237303e1f7101fee0e72d6ba630a3e4c5178" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/95f8237303e1f7101fee0e72d6ba630a3e4c5178", + "reference": "95f8237303e1f7101fee0e72d6ba630a3e4c5178", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/intl": "~3.4|~4.0", + "symfony/options-resolver": "~3.4|~4.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "~3.4|~4.0" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/dependency-injection": "<3.4", + "symfony/doctrine-bridge": "<3.4", + "symfony/framework-bundle": "<3.4", + "symfony/http-kernel": "<3.4", + "symfony/twig-bridge": "<3.4.5|<4.0.5,>=4.0" + }, + "require-dev": { + "doctrine/collections": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/http-foundation": "~3.4|~4.0", + "symfony/http-kernel": "~3.4|~4.0", + "symfony/security-csrf": "~3.4|~4.0", + "symfony/translation": "~3.4|~4.0", + "symfony/validator": "~3.4|~4.0", + "symfony/var-dumper": "~3.4|~4.0" + }, + "suggest": { + "symfony/framework-bundle": "For templating with PHP.", + "symfony/security-csrf": "For protecting forms against CSRF attacks.", + "symfony/twig-bridge": "For templating with Twig.", + "symfony/validator": "For form validation." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "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": "Symfony Form Component", + "homepage": "https://symfony.com", + "time": "2018-05-30T07:26:09+00:00" + }, { "name": "symfony/framework-bundle", "version": "v4.1.0", @@ -2584,6 +2665,81 @@ ], "time": "2018-05-01T23:02:13+00:00" }, + { + "name": "symfony/intl", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/intl.git", + "reference": "e2a48225f7d525b23a6e34caaa7320205abcf179" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/intl/zipball/e2a48225f7d525b23a6e34caaa7320205abcf179", + "reference": "e2a48225f7d525b23a6e34caaa7320205abcf179", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-intl-icu": "~1.0" + }, + "require-dev": { + "symfony/filesystem": "~3.4|~4.0" + }, + "suggest": { + "ext-intl": "to use the component with locales other than \"en\"" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Intl\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Eriksen Costa", + "email": "eriksen.costa@infranology.com.br" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A PHP replacement layer for the C intl extension that includes additional data from the ICU library.", + "homepage": "https://symfony.com", + "keywords": [ + "i18n", + "icu", + "internationalization", + "intl", + "l10n", + "localization" + ], + "time": "2018-05-30T07:26:09+00:00" + }, { "name": "symfony/lts", "version": "dev-master", @@ -2806,6 +2962,60 @@ ], "time": "2018-06-04T05:55:43+00:00" }, + { + "name": "symfony/options-resolver", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "9b9ab6043c57c8c5571bc846e6ebfd27dff3b589" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/9b9ab6043c57c8c5571bc846e6ebfd27dff3b589", + "reference": "9b9ab6043c57c8c5571bc846e6ebfd27dff3b589", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "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": "Symfony OptionsResolver Component", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "time": "2018-05-30T07:26:09+00:00" + }, { "name": "symfony/orm-pack", "version": "v1.0.5", @@ -2834,6 +3044,64 @@ "description": "A pack for the Doctrine ORM", "time": "2017-12-12T01:47:50+00:00" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "80ee17ae83c10cd513e5144f91a73607a21edb4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/80ee17ae83c10cd513e5144f91a73607a21edb4e", + "reference": "80ee17ae83c10cd513e5144f91a73607a21edb4e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/intl": "~2.3|~3.0|~4.0" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "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" + ], + "time": "2018-04-25T14:53:50+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.8.0", @@ -3665,6 +3933,92 @@ "homepage": "https://symfony.com", "time": "2018-05-16T14:41:07+00:00" }, + { + "name": "symfony/validator", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "f07bc14af6759710fb4639811a8240650d89786a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/f07bc14af6759710fb4639811a8240650d89786a", + "reference": "f07bc14af6759710fb4639811a8240650d89786a", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation": "~3.4|~4.0" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/dependency-injection": "<3.4", + "symfony/http-kernel": "<3.4", + "symfony/intl": "<4.1", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "doctrine/cache": "~1.0", + "egulias/email-validator": "^1.2.8|~2.0", + "symfony/cache": "~3.4|~4.0", + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/expression-language": "~3.4|~4.0", + "symfony/http-foundation": "~4.1", + "symfony/http-kernel": "~3.4|~4.0", + "symfony/intl": "~4.1", + "symfony/property-access": "~3.4|~4.0", + "symfony/var-dumper": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", + "doctrine/cache": "For using the default cached annotation reader and metadata cache.", + "egulias/email-validator": "Strict (RFC compliant) email validation", + "psr/cache-implementation": "For using the metadata cache.", + "symfony/config": "", + "symfony/expression-language": "For using the Expression validator", + "symfony/http-foundation": "", + "symfony/intl": "", + "symfony/property-access": "For accessing properties within comparison constraints", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "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": "Symfony Validator Component", + "homepage": "https://symfony.com", + "time": "2018-05-30T07:26:09+00:00" + }, { "name": "symfony/web-server-bundle", "version": "v4.1.0", diff --git a/src/Entity/Invite.php b/src/Entity/Invite.php new file mode 100644 index 0000000..bfdbc85 --- /dev/null +++ b/src/Entity/Invite.php @@ -0,0 +1,70 @@ +user = $user; + $this->code = md5(random_bytes(100)); + } + + public function getId(): int + { + return $this->id; + } + + public function getUser(): User + { + return $this->user; + } + + public function getCode(): string + { + return $this->code; + } + + public function getUsedBy(): ?User + { + return $this->usedBy; + } +} \ No newline at end of file diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..8dec741 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,121 @@ +username = $username; + $this->password = $password; + $this->email = $email; + } + + public function getId(): int + { + return $this->id; + } + + public function getUsername() + { + return $this->username; + } + + public function getPassword() + { + return $this->password; + } + + public function getSalt() + { + // Salt is not needed when using Argon2i + // @see https://symfony.com/doc/current/reference/configuration/security.html#using-the-argon2i-password-encoder + return null; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getRoles() + { + return ['ROLE_USER']; + } + + public function eraseCredentials() + { + + } + + /** @return Invite[]|ArrayCollection */ + public function getInvites(): \Traversable + { + return $this->invites; + } + + /** @see \Serializable::serialize() */ + public function serialize() + { + return serialize([ + $this->id, + $this->username, + $this->password, + ]); + } + + /** @see \Serializable::unserialize() */ + public function unserialize($serialized) + { + list( + $this->id, + $this->username, + $this->password + ) = unserialize($serialized, ['allowed_classes' => false]); + } +} \ No newline at end of file diff --git a/src/Form/CreateUserRequest.php b/src/Form/CreateUserRequest.php new file mode 100644 index 0000000..7cb7c3f --- /dev/null +++ b/src/Form/CreateUserRequest.php @@ -0,0 +1,39 @@ +add('username', TextType::class) + ->add('password', PasswordType::class) + ->add('email', EmailType::class) + ->add('inviteCode', TextType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => CreateUserRequest::class, + ]); + } + +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index 2aca2c7..66961d2 100644 --- a/symfony.lock +++ b/symfony.lock @@ -146,6 +146,9 @@ "ref": "e921bdbfe20cdefa3b82f379d1cd36df1bc8d115" } }, + "symfony/form": { + "version": "v4.1.0" + }, "symfony/framework-bundle": { "version": "3.3", "recipe": { @@ -164,6 +167,9 @@ "symfony/inflector": { "version": "v4.1.0" }, + "symfony/intl": { + "version": "v4.1.0" + }, "symfony/lts": { "version": "4-dev" }, @@ -179,9 +185,15 @@ "ref": "371d1a2b69984710646b09a1182ef1d4308c904f" } }, + "symfony/options-resolver": { + "version": "v4.1.0" + }, "symfony/orm-pack": { "version": "v1.0.5" }, + "symfony/polyfill-intl-icu": { + "version": "v1.8.0" + }, "symfony/polyfill-mbstring": { "version": "v1.8.0" }, @@ -245,6 +257,9 @@ "ref": "f75ac166398e107796ca94cc57fa1edaa06ec47f" } }, + "symfony/validator": { + "version": "v4.1.0" + }, "symfony/var-dumper": { "version": "v4.1.0" }, From 3212f29b3da63ae691bd6ec676a9940fdaceb72e Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Sun, 24 Jun 2018 21:24:43 +0300 Subject: [PATCH 09/24] Probably fixing PostgreSQL connection failure because of incorrect database URL. --- .env.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.dist b/.env.dist index f256ff0..e029635 100644 --- a/.env.dist +++ b/.env.dist @@ -13,6 +13,6 @@ APP_SECRET=xxx # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # Configure your db driver and server_version in config/packages/doctrine.yaml -APP_DATABASE_URL=pdo_pgsql://$PGUSER:$PGPASSWORD@127.0.0.1:5436/test?application_name=magnetico_web +APP_DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@127.0.0.1:5436/test?application_name=magnetico_web MAGNETICOD_DATABASE_URL=sqlite:///%kernel.project_dir%/tests/database/database.sqlite3 ###< doctrine/doctrine-bundle ### From d953e8a31948d2db6076628b63afddbd60cf2a6d Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Mon, 25 Jun 2018 01:42:26 +0300 Subject: [PATCH 10/24] User registration implemented: registration form, user:add command. User creation time and roles are now stored in the DB. Simple invite system implemented. --- config/packages/security.yaml | 6 ++ config/packages/twig.yaml | 1 + config/routes.yaml | 7 ++ src/Command/AddUserCommand.php | 98 +++++++++++++++++++ src/Controller/UserController.php | 64 ++++++++++++ src/Entity/Invite.php | 20 +++- src/Entity/User.php | 39 +++++++- src/Form/CreateUserRequest.php | 5 +- src/Repository/InviteRepository.php | 20 ++++ src/Repository/UserRepository.php | 20 ++++ src/User/Exception/InvalidInviteException.php | 8 ++ src/User/UserManager.php | 57 +++++++++++ templates/User/register.html.twig | 7 ++ 13 files changed, 344 insertions(+), 8 deletions(-) create mode 100644 src/Command/AddUserCommand.php create mode 100644 src/Controller/UserController.php create mode 100644 src/Repository/InviteRepository.php create mode 100644 src/Repository/UserRepository.php create mode 100644 src/User/Exception/InvalidInviteException.php create mode 100644 src/User/UserManager.php create mode 100644 templates/User/register.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index fb4c593..133e1c1 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -2,6 +2,12 @@ security: # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: in_memory: { memory: ~ } + encoders: + App\Entity\User: + algorithm: 'argon2i' + memory_cost: 16384 + time_cost: 2 + threads: 4 firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 3b315dc..454a794 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -2,3 +2,4 @@ twig: paths: ['%kernel.project_dir%/templates'] debug: '%kernel.debug%' strict_variables: '%kernel.debug%' + form_themes: ['bootstrap_4_layout.html.twig'] diff --git a/config/routes.yaml b/config/routes.yaml index 3c68b70..f75dc68 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -16,6 +16,13 @@ torrents_show: method: GET id: '\d+' +user_register: + path: /register/{inviteCode} + controller: App\Controller\UserController::register + requirements: + method: GET + inviteCode: \w{32} + # API api_v1_torrents: path: /api/v1/torrents diff --git a/src/Command/AddUserCommand.php b/src/Command/AddUserCommand.php new file mode 100644 index 0000000..f6361a0 --- /dev/null +++ b/src/Command/AddUserCommand.php @@ -0,0 +1,98 @@ +em = $em; + $this->userManager = $userManager; + $this->userRepo = $userRepo; + $this->inviteRepo = $inviterepo; + } + + protected function configure() + { + $this + ->setName('user:add') + ->addArgument('username', InputArgument::REQUIRED, 'Username') + ->addArgument('email', InputArgument::REQUIRED, 'Email') + ->addArgument('password', InputArgument::OPTIONAL, 'Password', null) + ->addOption('invites', 'i', InputOption::VALUE_OPTIONAL, 'Number of invites for user', 0) + ->addOption('role', 'r', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Role to add to the user') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $username = $input->getArgument('username'); + $email = $input->getArgument('email'); + $password = $input->getArgument('password'); + $invites = (int) $input->getOption('invites'); + $roles = (array) $input->getOption('role'); + + if (!$password) { + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelper('question'); + $question = new Question('Enter new user\'s password: '); + $question->setHidden(true); + $question->setHiddenFallback(false); + + $password = $questionHelper->ask($input, $output, $question); + } + + if (!$password) { + $output->writeln('User password cannot be empty.'); + + return 1; + } + + if ($roles) { + $user = $this->userManager->createUser($username, $password, $email, $roles); + } else { + $user = $this->userManager->createUser($username, $password, $email); + } + + + $this->userRepo->add($user); + + if ($invites) { + for ($i = 0; $i < $invites; $i++) { + $invite = new Invite($user); + $this->inviteRepo->add($invite); + } + } + + $this->em->flush(); + + $output->writeln(sprintf('User \'%s\' registered, %d invites added.', $user->getUsername(), $invites)); + + return 0; + } + +} \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..4960375 --- /dev/null +++ b/src/Controller/UserController.php @@ -0,0 +1,64 @@ +inviteCode = $inviteCode; + $form = $this->createRegisterForm($createUserRequest, $inviteCode); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + try { + $user = $userManager->createUserByInviteCode( + $createUserRequest->username, + $createUserRequest->password, + $createUserRequest->email, + $createUserRequest->inviteCode + ); + } catch (InvalidInviteException $ex) { + // @FIXME refactor InvalidInviteException to proper validator + $form->get('inviteCode')->addError(new FormError('Invalid invite code')); + + return $this->render('User/register.html.twig', ['form' => $form->createView()]); + } + + $userRepository->add($user); + $em->flush(); + + return $this->redirectToRoute('index'); + } + + return $this->render('User/register.html.twig', ['form' => $form->createView()]); + } + + private function createRegisterForm(CreateUserRequest $createUserRequest, string $inviteCode): FormInterface + { + $form = $this->createForm(CreateUserRequestType::class, $createUserRequest, [ + 'action' => $this->generateUrl('user_register', ['inviteCode' => $inviteCode]), + ]); + + $form->add('submit', SubmitType::class); + + return $form; + } +} \ No newline at end of file diff --git a/src/Entity/Invite.php b/src/Entity/Invite.php index bfdbc85..9b94836 100644 --- a/src/Entity/Invite.php +++ b/src/Entity/Invite.php @@ -6,7 +6,7 @@ use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="invites", schema="users") - * @ORM\Entity() + * @ORM\Entity(repositoryClass="App\Repository\InviteRepository") */ class Invite { @@ -42,9 +42,9 @@ class Invite */ private $usedBy; - public function __construct(User $user) + public function __construct(User $forUser) { - $this->user = $user; + $this->user = $forUser; $this->code = md5(random_bytes(100)); } @@ -67,4 +67,18 @@ class Invite { return $this->usedBy; } + + public function use(User $user): void + { + if ($this->usedBy) { + throw new \RuntimeException(sprintf( + 'Invite #%d is already used by User#%d and can\'t be used by User#%d', + $this->id, + $this->usedBy->getId(), + $user->getId() + )); + } + + $this->usedBy = $user; + } } \ No newline at end of file diff --git a/src/Entity/User.php b/src/Entity/User.php index 8dec741..e3b0b03 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -8,7 +8,7 @@ use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Table(name="users", schema="users") - * @ORM\Entity() + * @ORM\Entity(repositoryClass="App\Repository\UserRepository") */ class User implements UserInterface, \Serializable { @@ -42,6 +42,20 @@ class User implements UserInterface, \Serializable */ private $email; + /** + * @var string[] + * + * @ORM\Column(name="roles", type="json") + */ + private $roles = []; + + /** + * @var \DateTime + * + * @ORM\Column(name="created_at", type="datetime") + */ + private $createdAt; + /** * @var Invite[]|ArrayCollection * @@ -49,11 +63,13 @@ class User implements UserInterface, \Serializable */ private $invites; - public function __construct(string $username, string $password, string $email) + public function __construct(string $username, string $password, string $email, array $roles = []) { $this->username = $username; $this->password = $password; $this->email = $email; + $this->roles = $roles ?: ['ROLE_USER']; + $this->createdAt = new \DateTime(); } public function getId(): int @@ -71,6 +87,11 @@ class User implements UserInterface, \Serializable return $this->password; } + public function updatePassword(string $password): void + { + $this->password = $password; + } + public function getSalt() { // Salt is not needed when using Argon2i @@ -83,9 +104,19 @@ class User implements UserInterface, \Serializable return $this->email; } - public function getRoles() + public function getRoles(): array { - return ['ROLE_USER']; + return $this->roles; + } + + public function addRole(string $role): void + { + $this->roles[] = $role; + } + + public function getCreatedAt(): \DateTime + { + return $this->createdAt; } public function eraseCredentials() diff --git a/src/Form/CreateUserRequest.php b/src/Form/CreateUserRequest.php index 7cb7c3f..0647f39 100644 --- a/src/Form/CreateUserRequest.php +++ b/src/Form/CreateUserRequest.php @@ -4,6 +4,9 @@ namespace App\Form; use Symfony\Component\Validator\Constraints as Assert; +/** + * @todo implement UniqueEntity constraint for DTO and use it here + */ class CreateUserRequest { /** @@ -18,7 +21,7 @@ class CreateUserRequest * @var string * * @Assert\NotBlank() - * @Assert\Length(min="8") + * @Assert\Length(min="8", max="4096") */ public $password; diff --git a/src/Repository/InviteRepository.php b/src/Repository/InviteRepository.php new file mode 100644 index 0000000..58bf7b0 --- /dev/null +++ b/src/Repository/InviteRepository.php @@ -0,0 +1,20 @@ +getEntityManager()->persist($invite); + } +} \ No newline at end of file diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..c9b4c14 --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,20 @@ +getEntityManager()->persist($user); + } +} \ No newline at end of file diff --git a/src/User/Exception/InvalidInviteException.php b/src/User/Exception/InvalidInviteException.php new file mode 100644 index 0000000..be497ae --- /dev/null +++ b/src/User/Exception/InvalidInviteException.php @@ -0,0 +1,8 @@ +userRepo = $userRepo; + $this->inviteRepo = $inviteRepo; + $this->encoderFactory = $encoderFactory; + } + + public function createUser(string $username, string $password, string $email, array $roles = self::DEFAULT_ROLES): User + { + $encodedPassword = $this->encoderFactory->getEncoder(User::class)->encodePassword($password, null); + + $user = new User( + $username, + $encodedPassword, + $email, + $roles + ); + + return $user; + } + + public function createUserByInviteCode(string $username, string $password, string $email, string $inviteCode, array $roles = self::DEFAULT_ROLES): User + { + /** @var Invite $invite */ + if (null === $invite = $this->inviteRepo->findOneBy(['code' => $inviteCode, 'usedBy' => null])) { + throw new InvalidInviteException(); + } + + $user = $this->createUser($username, $password, $email,$roles); + + $invite->use($user); + + return $user; + } +} \ No newline at end of file diff --git a/templates/User/register.html.twig b/templates/User/register.html.twig new file mode 100644 index 0000000..50491ff --- /dev/null +++ b/templates/User/register.html.twig @@ -0,0 +1,7 @@ +{% extends 'base.html.twig' %} + +{% block content %} +
+ {{ form(form) }} +
+{% endblock %} \ No newline at end of file From ef29705a37939d2f3ac2550ab03d5da85ebc358f Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Mon, 25 Jun 2018 04:02:27 +0300 Subject: [PATCH 11/24] Basic login functionality added. --- config/packages/security.yaml | 36 ++++++++++++++++-------- config/routes.yaml | 9 +++++- src/Controller/MainController.php | 19 +++++++++++-- src/Controller/SecurityController.php | 40 +++++++++++++++++++++++++++ src/Form/LoginType.php | 26 +++++++++++++++++ templates/Security/login.html.twig | 7 +++++ templates/base.html.twig | 19 +++++++++---- templates/index.html.twig | 4 ++- 8 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 src/Controller/SecurityController.php create mode 100644 src/Form/LoginType.php create mode 100644 templates/Security/login.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 133e1c1..3433203 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,7 +1,11 @@ security: # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: - in_memory: { memory: ~ } + app_db_provider: + entity: + class: App\Entity\User + property: username + manager_name: default encoders: App\Entity\User: algorithm: 'argon2i' @@ -12,19 +16,29 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false - main: + api: + pattern: ^/api/ anonymous: true + main: + pattern: ^/ + anonymous: ~ + provider: app_db_provider + form_login: + login_path: user_login + check_path: user_login + logout: + path: user_logout + target: / + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 + path: / + always_remember_me: true - # activate different ways to authenticate - - # http_basic: true - # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate - - # form_login: true - # https://symfony.com/doc/current/security/form_login_setup.html # 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 } + - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: /login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/, roles: ROLE_USER } diff --git a/config/routes.yaml b/config/routes.yaml index f75dc68..a386f24 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -4,7 +4,7 @@ index: controller: App\Controller\MainController::index torrents_search: - path: /torrents/search + path: /torrents controller: App\Controller\TorrentController::searchTorrent requirements: method: GET @@ -23,6 +23,13 @@ user_register: method: GET inviteCode: \w{32} +user_login: + path: /login + controller: App\Controller\SecurityController::login + +user_logout: + path: /logout + # API api_v1_torrents: path: /api/v1/torrents diff --git a/src/Controller/MainController.php b/src/Controller/MainController.php index e2125a5..603c639 100644 --- a/src/Controller/MainController.php +++ b/src/Controller/MainController.php @@ -2,16 +2,29 @@ namespace App\Controller; -use App\Magnetico\Repository\TorrentRepository; +use App\Form\LoginType; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Response; class MainController extends Controller { - public function index(TorrentRepository $repo): Response + public function index(): Response { return $this->render('index.html.twig', [ - 'torrentsCount' => $repo->getTorrentsTotalCount(), + 'loginForm' => $this->createLoginForm('')->createView(), ]); } + + private function createLoginForm(string $username): FormInterface + { + $form = $this->createForm(LoginType::class, null, [ + 'action' => $this->generateUrl('user_login'), + ]); + $form->get('_username')->setData($username); + $form->add('submit', SubmitType::class); + + return $form; + } } \ No newline at end of file diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..50e6ccf --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,40 @@ +getLastAuthenticationError() ? $authenticationUtils->getLastAuthenticationError()->getMessage() : ''; + $lastUsername = $authenticationUtils->getLastUsername(); + + $form = $this->createLoginForm($lastUsername); + $form->handleRequest($request); + + if ($lastError) { + $form->addError(new FormError($lastError)); + } + + return $this->render('Security/login.html.twig', ['form' => $form->createView()]); + } + + private function createLoginForm(string $username): FormInterface + { + $form = $this->createForm(LoginType::class, null, [ + 'action' => $this->generateUrl('user_login'), + ]); + $form->get('_username')->setData($username); + $form->add('submit', SubmitType::class); + + return $form; + } +} \ No newline at end of file diff --git a/src/Form/LoginType.php b/src/Form/LoginType.php new file mode 100644 index 0000000..d5bb8ed --- /dev/null +++ b/src/Form/LoginType.php @@ -0,0 +1,26 @@ +add('_username', TextType::class, ['mapped' => false]) + ->add('_password', PasswordType::class, ['mapped' => false]) + ; + } + + public function getBlockPrefix() + { + // Empty prefix for default UsernamePasswordFrormAuthenticationListener + return ''; + } + + +} \ No newline at end of file diff --git a/templates/Security/login.html.twig b/templates/Security/login.html.twig new file mode 100644 index 0000000..0ec5005 --- /dev/null +++ b/templates/Security/login.html.twig @@ -0,0 +1,7 @@ +{% extends 'base.html.twig' %} + +{% block content %} +
+ {{ form(form) }} +
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html.twig b/templates/base.html.twig index 16e1876..9601fd5 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -26,16 +26,25 @@ diff --git a/templates/index.html.twig b/templates/index.html.twig index 24e45fb..9cea746 100644 --- a/templates/index.html.twig +++ b/templates/index.html.twig @@ -2,6 +2,8 @@ {% block content %}
-

Torrents indexed: {{ torrentsCount }}

+ {% if not is_granted('ROLE_USER') %} + Login + {% endif %}
{% endblock %} \ No newline at end of file From 182d4f49faf33e64a3090ee34197af1c1459da08 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Mon, 25 Jun 2018 19:59:51 +0300 Subject: [PATCH 12/24] Api firewall is now defined as stateless. --- config/packages/security.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 3433203..0f01350 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -18,6 +18,7 @@ security: security: false api: pattern: ^/api/ + stateless: true anonymous: true main: pattern: ^/ From 69a2326c3e4c3f2d8c821828694e7cc746984564 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Mon, 25 Jun 2018 20:43:04 +0300 Subject: [PATCH 13/24] API access is now temporarily anonymous. --- config/packages/security.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 0f01350..1baad65 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -40,6 +40,7 @@ security: # 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: ^/api/, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/, roles: ROLE_USER } From c5d2f68c571402e27f04720470c1961be88fff1e Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Mon, 25 Jun 2018 22:39:47 +0300 Subject: [PATCH 14/24] Service autowiring exclude rules updated. --- config/services.yaml | 10 +++++++++- src/Controller/UserController.php | 3 ++- src/Form/CreateUserRequestType.php | 5 ++--- src/{Form => FormRequest}/CreateUserRequest.php | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) rename src/{Form => FormRequest}/CreateUserRequest.php (95%) diff --git a/config/services.yaml b/config/services.yaml index 4e0021b..da562e0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -17,12 +17,20 @@ services: App\: resource: '../src/*' - exclude: '../src/{Entity,Migrations,Tests,Kernel.php}' + exclude: '../src/{Api/V1/{DTO},Magnetico/{Entity,Migrations},Entity,FormRequest,Migrations,Tests,Kernel.php}' + # Use array in exclude config from Symfony 4.2 + #- '../src/Api/V1/{DTO}' + #- '../src/Magnetico/{Entity,Migrations}' + #- '../src/{Entity,FormRequest,Migrations,Tests,Kernel.php}' App\Controller\: resource: '../src/Controller' tags: ['controller.service_arguments'] + App\Api\V1\Controller\: + resource: '../src/Api/V1/Controller' + tags: ['controller.service_arguments'] + # Fast normalizer for Symfony Serializer get_set_method_normalizer: class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 4960375..b4fee2f 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,7 +2,8 @@ namespace App\Controller; -use App\Form\{CreateUserRequest, CreateUserRequestType}; +use App\Form\{CreateUserRequestType}; +use App\FormRequest\CreateUserRequest; use App\Repository\{UserRepository}; use App\User\Exception\InvalidInviteException; use App\User\UserManager; diff --git a/src/Form/CreateUserRequestType.php b/src/Form/CreateUserRequestType.php index b83492d..9f076a2 100644 --- a/src/Form/CreateUserRequestType.php +++ b/src/Form/CreateUserRequestType.php @@ -2,10 +2,9 @@ namespace App\Form; +use App\FormRequest\CreateUserRequest; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\EmailType; -use Symfony\Component\Form\Extension\Core\Type\PasswordType; -use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\{EmailType, PasswordType, TextType}; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; diff --git a/src/Form/CreateUserRequest.php b/src/FormRequest/CreateUserRequest.php similarity index 95% rename from src/Form/CreateUserRequest.php rename to src/FormRequest/CreateUserRequest.php index 0647f39..6016905 100644 --- a/src/Form/CreateUserRequest.php +++ b/src/FormRequest/CreateUserRequest.php @@ -1,6 +1,6 @@ Date: Tue, 26 Jun 2018 00:56:32 +0300 Subject: [PATCH 15/24] API token auth using Guard implementation added. --- config/packages/security.yaml | 12 ++- config/routes.yaml | 9 ++ .../V1/Controller/AbstractApiController.php | 19 +++++ src/Api/V1/Controller/SecurityController.php | 13 +++ src/Api/V1/Controller/TorrentController.php | 20 ++--- src/Entity/ApiToken.php | 57 +++++++++++++ src/Repository/ApiTokenRepository.php | 32 +++++++ src/Security/ApiTokenAuthenticator.php | 85 +++++++++++++++++++ src/Security/ApiTokenUserProvider.php | 39 +++++++++ 9 files changed, 269 insertions(+), 17 deletions(-) create mode 100644 src/Api/V1/Controller/AbstractApiController.php create mode 100644 src/Api/V1/Controller/SecurityController.php create mode 100644 src/Entity/ApiToken.php create mode 100644 src/Repository/ApiTokenRepository.php create mode 100644 src/Security/ApiTokenAuthenticator.php create mode 100644 src/Security/ApiTokenUserProvider.php diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 1baad65..106c86a 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,11 +1,13 @@ security: # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: - app_db_provider: + default_provider: entity: class: App\Entity\User property: username manager_name: default + api_token_provider: + id: App\Security\ApiTokenUserProvider encoders: App\Entity\User: algorithm: 'argon2i' @@ -20,10 +22,13 @@ security: pattern: ^/api/ stateless: true anonymous: true + simple_preauth: + authenticator: App\Security\ApiTokenAuthenticator + provider: api_token_provider main: pattern: ^/ anonymous: ~ - provider: app_db_provider + provider: default_provider form_login: login_path: user_login check_path: user_login @@ -40,7 +45,8 @@ security: # 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: ^/api/, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/api/v1/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/api/, roles: ROLE_USER } - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/, roles: ROLE_USER } diff --git a/config/routes.yaml b/config/routes.yaml index a386f24..20ddb7f 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -31,6 +31,15 @@ user_logout: path: /logout # API +api_login: + path: /api/v1/login + controller: App\Api\V1\Controller\SecurityController::login + defaults: + _format: json + requirements: + method: POST + _format: json + api_v1_torrents: path: /api/v1/torrents controller: App\Api\V1\Controller\TorrentController::search diff --git a/src/Api/V1/Controller/AbstractApiController.php b/src/Api/V1/Controller/AbstractApiController.php new file mode 100644 index 0000000..fb0a90c --- /dev/null +++ b/src/Api/V1/Controller/AbstractApiController.php @@ -0,0 +1,19 @@ +json(new ApiResponse($data, $code, $message, $status), $code, [], [ + 'groups' => array_merge(self::DEFAULT_SERIALIZER_GROUPS,$groups), + ]); + } +} \ No newline at end of file diff --git a/src/Api/V1/Controller/SecurityController.php b/src/Api/V1/Controller/SecurityController.php new file mode 100644 index 0000000..46c3a6f --- /dev/null +++ b/src/Api/V1/Controller/SecurityController.php @@ -0,0 +1,13 @@ +query->get('query', ''); $page = (int) $request->query->get('page', '1'); @@ -29,16 +25,12 @@ class TorrentController extends Controller ->setMaxPerPage(self::PER_PAGE) ; - return $this->json(new ApiResponse(ListPage::createFromPager($pager)),Response::HTTP_OK, [], [ - 'groups' => array_merge(self::DEFAULT_SERIALIZER_GROUPS,['api_v1_search']), - ]); + return $this->createJsonResponse(ListPage::createFromPager($pager), ['api_v1_search']); } - public function show(Torrent $torrent): Response + public function show(Torrent $torrent): JsonResponse { - return $this->json(new ApiResponse($torrent), Response::HTTP_OK, [], [ - 'groups' => array_merge(self::DEFAULT_SERIALIZER_GROUPS,['api_v1_show']), - ]); + return $this->createJsonResponse($torrent, ['api_v1_show'], JsonResponse::HTTP_OK,null, ''); } diff --git a/src/Entity/ApiToken.php b/src/Entity/ApiToken.php new file mode 100644 index 0000000..61ef9d1 --- /dev/null +++ b/src/Entity/ApiToken.php @@ -0,0 +1,57 @@ +user = $user; + $this->key = md5(random_bytes(100)); + $this->createdAt = new \DateTime(); + } + + public function getUser(): User + { + return $this->user; + } + + public function getKey(): string + { + return $this->key; + } + + public function getCreatedAt(): \DateTime + { + return $this->createdAt; + } +} \ No newline at end of file diff --git a/src/Repository/ApiTokenRepository.php b/src/Repository/ApiTokenRepository.php new file mode 100644 index 0000000..0a7847b --- /dev/null +++ b/src/Repository/ApiTokenRepository.php @@ -0,0 +1,32 @@ +getEntityManager()->persist($token); + } + + public function findUserByTokenKey(string $tokenKey): ?User + { + $qb = $this->createQueryBuilder('at'); + $qb + ->select('at.user') + ->where('at.key = :tokenKey') + ->setParameter('tokenKey', $tokenKey) + ; + + return $qb->getQuery()->getOneOrNullResult(); + } +} \ No newline at end of file diff --git a/src/Security/ApiTokenAuthenticator.php b/src/Security/ApiTokenAuthenticator.php new file mode 100644 index 0000000..7610bd4 --- /dev/null +++ b/src/Security/ApiTokenAuthenticator.php @@ -0,0 +1,85 @@ +serializer = $serializer; + } + + /** Takes request data and creates token which will be ready to auth check */ + public function createToken(Request $request, $providerKey) + { + if (!($tokenKey = $request->headers->get(self::TOKEN_HEADER))) { + throw new BadCredentialsException(sprintf('\'%s\' is invalid or not defined', self::TOKEN_HEADER)); + } + + return new PreAuthenticatedToken( + 'anon.', + $tokenKey, + $providerKey + ); + } + + public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) + { + if (!$userProvider instanceof ApiTokenUserProvider) { + throw new \InvalidArgumentException(sprintf( + 'The user provider for providerKey = \'%s\' must be an instance of %s, %s given.', + $providerKey, + ApiTokenUserProvider::class, + get_class($userProvider) + )); + } + + $apiTokenKey = $token->getCredentials(); + + $user = $userProvider->loadUserByUsername($apiTokenKey); + + if (!$user) { + throw new CustomUserMessageAuthenticationException(sprintf( + 'API token \'%s\' does not exist.', $apiTokenKey + )); + } + + return new PreAuthenticatedToken( + $user, + $apiTokenKey, + $providerKey, + $user->getRoles() + ); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse + { + // @todo Decouple with App\Api\V1\DTO + $json = $this->serializer->serialize( + new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $exception->getMessage()), + 'json', + ['groups' => ['api_v1']] + ); + + return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED,[], true); + } + + + public function supportsToken(TokenInterface $token, $providerKey) + { + return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey; + } +} \ No newline at end of file diff --git a/src/Security/ApiTokenUserProvider.php b/src/Security/ApiTokenUserProvider.php new file mode 100644 index 0000000..85f01b4 --- /dev/null +++ b/src/Security/ApiTokenUserProvider.php @@ -0,0 +1,39 @@ +userRepo = $userRepo; + } + + public function loadUserByUsername($username): User + { + if (null === $user = $this->userRepo->findUserByTokenKey($username)) { + throw new UsernameNotFoundException(sprintf('Token \'%s\' is not found.', $username)); + } + + return $user; + } + + public function refreshUser(UserInterface $user) + { + throw new UnsupportedUserException(); + } + + public function supportsClass($class): bool + { + return User::class === $class; + } + +} \ No newline at end of file From 43d5d39a92ecbe825543b453072f49966bc78915 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Tue, 26 Jun 2018 20:34:35 +0300 Subject: [PATCH 16/24] anonymous set to '~' in 'api' firewall. --- config/packages/security.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 106c86a..3d84a75 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -20,8 +20,8 @@ security: security: false api: pattern: ^/api/ + anonymous: ~ stateless: true - anonymous: true simple_preauth: authenticator: App\Security\ApiTokenAuthenticator provider: api_token_provider From a87bb2be30b8f63581daff84978c781bae773e19 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Tue, 26 Jun 2018 20:40:25 +0300 Subject: [PATCH 17/24] composer update. --- composer.lock | 298 +++++++++++++++++++++++++------------------------- 1 file changed, 148 insertions(+), 150 deletions(-) diff --git a/composer.lock b/composer.lock index aa6943c..d38298a 100644 --- a/composer.lock +++ b/composer.lock @@ -1657,16 +1657,16 @@ }, { "name": "symfony/cache", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "4986efce97c002e58380e8c0474acbf72eda9339" + "reference": "be95ef3665747e6ff9d883a8adc87085769009f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/4986efce97c002e58380e8c0474acbf72eda9339", - "reference": "4986efce97c002e58380e8c0474acbf72eda9339", + "url": "https://api.github.com/repos/symfony/cache/zipball/be95ef3665747e6ff9d883a8adc87085769009f0", + "reference": "be95ef3665747e6ff9d883a8adc87085769009f0", "shasum": "" }, "require": { @@ -1722,20 +1722,20 @@ "caching", "psr6" ], - "time": "2018-05-16T14:33:22+00:00" + "time": "2018-06-22T08:59:39+00:00" }, { "name": "symfony/config", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "5ceefc256caecc3e25147c4e5b933de71d0020c4" + "reference": "e57e7b573df9d0eaa8c0152768c708ee7ea2b8e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/5ceefc256caecc3e25147c4e5b933de71d0020c4", - "reference": "5ceefc256caecc3e25147c4e5b933de71d0020c4", + "url": "https://api.github.com/repos/symfony/config/zipball/e57e7b573df9d0eaa8c0152768c708ee7ea2b8e5", + "reference": "e57e7b573df9d0eaa8c0152768c708ee7ea2b8e5", "shasum": "" }, "require": { @@ -1785,20 +1785,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-05-16T14:33:22+00:00" + "time": "2018-06-20T11:15:17+00:00" }, { "name": "symfony/console", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2d5d973bf9933d46802b01010bd25c800c87c242" + "reference": "70591cda56b4b47c55776ac78e157c4bb6c8b43f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2d5d973bf9933d46802b01010bd25c800c87c242", - "reference": "2d5d973bf9933d46802b01010bd25c800c87c242", + "url": "https://api.github.com/repos/symfony/console/zipball/70591cda56b4b47c55776ac78e157c4bb6c8b43f", + "reference": "70591cda56b4b47c55776ac78e157c4bb6c8b43f", "shasum": "" }, "require": { @@ -1853,20 +1853,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-05-31T10:17:53+00:00" }, { "name": "symfony/debug", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "449f8b00b28ab6e6912c3e6b920406143b27193b" + "reference": "dbe0fad88046a755dcf9379f2964c61a02f5ae3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/449f8b00b28ab6e6912c3e6b920406143b27193b", - "reference": "449f8b00b28ab6e6912c3e6b920406143b27193b", + "url": "https://api.github.com/repos/symfony/debug/zipball/dbe0fad88046a755dcf9379f2964c61a02f5ae3d", + "reference": "dbe0fad88046a755dcf9379f2964c61a02f5ae3d", "shasum": "" }, "require": { @@ -1909,20 +1909,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-05-16T14:33:22+00:00" + "time": "2018-06-08T09:39:36+00:00" }, { "name": "symfony/dependency-injection", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "f2a3f0dc640a28b8aedd51b47ad6e6c5cebb3c00" + "reference": "e761828a85d7dfc00b927f94ccbe1851ce0b6535" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f2a3f0dc640a28b8aedd51b47ad6e6c5cebb3c00", - "reference": "f2a3f0dc640a28b8aedd51b47ad6e6c5cebb3c00", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e761828a85d7dfc00b927f94ccbe1851ce0b6535", + "reference": "e761828a85d7dfc00b927f94ccbe1851ce0b6535", "shasum": "" }, "require": { @@ -1930,7 +1930,7 @@ "psr/container": "^1.0" }, "conflict": { - "symfony/config": "<4.1", + "symfony/config": "<4.1.1", "symfony/finder": "<3.4", "symfony/proxy-manager-bridge": "<3.4", "symfony/yaml": "<3.4" @@ -1980,24 +1980,24 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-05-25T14:55:38+00:00" + "time": "2018-06-25T11:12:43+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "9d361867451d5397e46bb32056fa43921545676e" + "reference": "a7751cc8d949c16366976633678116f85662b989" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/9d361867451d5397e46bb32056fa43921545676e", - "reference": "9d361867451d5397e46bb32056fa43921545676e", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/a7751cc8d949c16366976633678116f85662b989", + "reference": "a7751cc8d949c16366976633678116f85662b989", "shasum": "" }, "require": { - "doctrine/common": "~2.4", + "doctrine/common": "~2.4@stable", "php": "^7.1.3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0" @@ -2060,11 +2060,11 @@ ], "description": "Symfony Doctrine Bridge", "homepage": "https://symfony.com", - "time": "2018-05-16T14:41:07+00:00" + "time": "2018-06-25T11:31:22+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -2127,7 +2127,7 @@ }, { "name": "symfony/filesystem", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -2177,16 +2177,16 @@ }, { "name": "symfony/finder", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "087e2ee0d74464a4c6baac4e90417db7477dc238" + "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/087e2ee0d74464a4c6baac4e90417db7477dc238", - "reference": "087e2ee0d74464a4c6baac4e90417db7477dc238", + "url": "https://api.github.com/repos/symfony/finder/zipball/84714b8417d19e4ba02ea78a41a975b3efaafddb", + "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb", "shasum": "" }, "require": { @@ -2222,7 +2222,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-05-16T14:33:22+00:00" + "time": "2018-06-19T21:38:16+00:00" }, { "name": "symfony/flex", @@ -2272,16 +2272,16 @@ }, { "name": "symfony/form", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "95f8237303e1f7101fee0e72d6ba630a3e4c5178" + "reference": "cf9ed8b1a17b34d52c458352cb0c29838ee5f825" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/95f8237303e1f7101fee0e72d6ba630a3e4c5178", - "reference": "95f8237303e1f7101fee0e72d6ba630a3e4c5178", + "url": "https://api.github.com/repos/symfony/form/zipball/cf9ed8b1a17b34d52c458352cb0c29838ee5f825", + "reference": "cf9ed8b1a17b34d52c458352cb0c29838ee5f825", "shasum": "" }, "require": { @@ -2349,20 +2349,20 @@ ], "description": "Symfony Form Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-06-22T08:59:39+00:00" }, { "name": "symfony/framework-bundle", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "e93974e78872d22cceebf401ce230363b192268e" + "reference": "a34630e9712b23fb0a20cc12fe937a9ddcaacbe8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/e93974e78872d22cceebf401ce230363b192268e", - "reference": "e93974e78872d22cceebf401ce230363b192268e", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a34630e9712b23fb0a20cc12fe937a9ddcaacbe8", + "reference": "a34630e9712b23fb0a20cc12fe937a9ddcaacbe8", "shasum": "" }, "require": { @@ -2370,7 +2370,7 @@ "php": "^7.1.3", "symfony/cache": "~3.4|~4.0", "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "^4.1", + "symfony/dependency-injection": "^4.1.1", "symfony/event-dispatcher": "^4.1", "symfony/filesystem": "~3.4|~4.0", "symfony/finder": "~3.4|~4.0", @@ -2406,7 +2406,7 @@ "symfony/expression-language": "~3.4|~4.0", "symfony/form": "^4.1", "symfony/lock": "~3.4|~4.0", - "symfony/messenger": "^4.1-beta2", + "symfony/messenger": "^4.1", "symfony/polyfill-intl-icu": "~1.0", "symfony/process": "~3.4|~4.0", "symfony/property-info": "~3.4|~4.0", @@ -2464,20 +2464,20 @@ ], "description": "Symfony FrameworkBundle", "homepage": "https://symfony.com", - "time": "2018-05-30T09:26:42+00:00" + "time": "2018-06-20T21:41:56+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "a916c88390fb861ee21f12a92b107d51bb68af99" + "reference": "4f9c7cf962e635b0b26b14500ac046e07dbef7f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a916c88390fb861ee21f12a92b107d51bb68af99", - "reference": "a916c88390fb861ee21f12a92b107d51bb68af99", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4f9c7cf962e635b0b26b14500ac046e07dbef7f3", + "reference": "4f9c7cf962e635b0b26b14500ac046e07dbef7f3", "shasum": "" }, "require": { @@ -2518,20 +2518,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-05-25T14:55:38+00:00" + "time": "2018-06-19T21:38:16+00:00" }, { "name": "symfony/http-kernel", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "b5ab9d4cdbfd369083744b6b5dfbf454e31e5f90" + "reference": "29c094a1c4f8209b7e033f612cbbd69029e38955" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b5ab9d4cdbfd369083744b6b5dfbf454e31e5f90", - "reference": "b5ab9d4cdbfd369083744b6b5dfbf454e31e5f90", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/29c094a1c4f8209b7e033f612cbbd69029e38955", + "reference": "29c094a1c4f8209b7e033f612cbbd69029e38955", "shasum": "" }, "require": { @@ -2539,13 +2539,13 @@ "psr/log": "~1.0", "symfony/debug": "~3.4|~4.0", "symfony/event-dispatcher": "~4.1", - "symfony/http-foundation": "~4.1", + "symfony/http-foundation": "^4.1.1", "symfony/polyfill-ctype": "~1.8" }, "conflict": { "symfony/config": "<3.4", "symfony/dependency-injection": "<4.1", - "symfony/var-dumper": "<4.1", + "symfony/var-dumper": "<4.1.1", "twig/twig": "<1.34|<2.4,>=2" }, "provide": { @@ -2566,7 +2566,7 @@ "symfony/stopwatch": "~3.4|~4.0", "symfony/templating": "~3.4|~4.0", "symfony/translation": "~3.4|~4.0", - "symfony/var-dumper": "~4.1" + "symfony/var-dumper": "^4.1.1" }, "suggest": { "symfony/browser-kit": "", @@ -2605,11 +2605,11 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-05-30T12:52:34+00:00" + "time": "2018-06-25T13:06:45+00:00" }, { "name": "symfony/inflector", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/inflector.git", @@ -2667,16 +2667,16 @@ }, { "name": "symfony/intl", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "e2a48225f7d525b23a6e34caaa7320205abcf179" + "reference": "cb21b901892c0d637f9c2f50860ed2fe29ce4b55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/e2a48225f7d525b23a6e34caaa7320205abcf179", - "reference": "e2a48225f7d525b23a6e34caaa7320205abcf179", + "url": "https://api.github.com/repos/symfony/intl/zipball/cb21b901892c0d637f9c2f50860ed2fe29ce4b55", + "reference": "cb21b901892c0d637f9c2f50860ed2fe29ce4b55", "shasum": "" }, "require": { @@ -2738,7 +2738,7 @@ "l10n", "localization" ], - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-06-25T11:12:43+00:00" }, { "name": "symfony/lts", @@ -2835,16 +2835,16 @@ }, { "name": "symfony/monolog-bridge", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "c0fe8631641b370f926c9dc54f9cac154a9e503b" + "reference": "0a7cac4e6f2eb72428367893d6d2fc2883b4aeba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/c0fe8631641b370f926c9dc54f9cac154a9e503b", - "reference": "c0fe8631641b370f926c9dc54f9cac154a9e503b", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/0a7cac4e6f2eb72428367893d6d2fc2883b4aeba", + "reference": "0a7cac4e6f2eb72428367893d6d2fc2883b4aeba", "shasum": "" }, "require": { @@ -2897,7 +2897,7 @@ ], "description": "Symfony Monolog Bridge", "homepage": "https://symfony.com", - "time": "2018-05-16T14:33:22+00:00" + "time": "2018-05-31T10:17:53+00:00" }, { "name": "symfony/monolog-bundle", @@ -2964,16 +2964,16 @@ }, { "name": "symfony/options-resolver", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "9b9ab6043c57c8c5571bc846e6ebfd27dff3b589" + "reference": "45cdcc8a96ef92b43a50723e6d1f5f83096e8cef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/9b9ab6043c57c8c5571bc846e6ebfd27dff3b589", - "reference": "9b9ab6043c57c8c5571bc846e6ebfd27dff3b589", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/45cdcc8a96ef92b43a50723e6d1f5f83096e8cef", + "reference": "45cdcc8a96ef92b43a50723e6d1f5f83096e8cef", "shasum": "" }, "require": { @@ -3014,7 +3014,7 @@ "configuration", "options" ], - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-05-31T10:17:53+00:00" }, { "name": "symfony/orm-pack", @@ -3163,16 +3163,16 @@ }, { "name": "symfony/process", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "73445bd33b0d337c060eef9652b94df72b6b3434" + "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/73445bd33b0d337c060eef9652b94df72b6b3434", - "reference": "73445bd33b0d337c060eef9652b94df72b6b3434", + "url": "https://api.github.com/repos/symfony/process/zipball/1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a", + "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a", "shasum": "" }, "require": { @@ -3208,11 +3208,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-05-31T10:17:53+00:00" }, { "name": "symfony/property-access", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", @@ -3279,7 +3279,7 @@ }, { "name": "symfony/property-info", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", @@ -3355,16 +3355,16 @@ }, { "name": "symfony/routing", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "180b51c66d10f09e562c9ebc395b39aacb2cf8a2" + "reference": "b38b9797327b26ea2e4146a40e6e2dc9820a6932" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/180b51c66d10f09e562c9ebc395b39aacb2cf8a2", - "reference": "180b51c66d10f09e562c9ebc395b39aacb2cf8a2", + "url": "https://api.github.com/repos/symfony/routing/zipball/b38b9797327b26ea2e4146a40e6e2dc9820a6932", + "reference": "b38b9797327b26ea2e4146a40e6e2dc9820a6932", "shasum": "" }, "require": { @@ -3377,7 +3377,6 @@ }, "require-dev": { "doctrine/annotations": "~1.0", - "doctrine/common": "~2.2", "psr/log": "~1.0", "symfony/config": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0", @@ -3429,20 +3428,20 @@ "uri", "url" ], - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-06-19T21:38:16+00:00" }, { "name": "symfony/security", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/security.git", - "reference": "2ba804b4af205c060094c28cece0b42a26a67537" + "reference": "fa46e38ff4dea2d3949630efd33ed73e2ac0850a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security/zipball/2ba804b4af205c060094c28cece0b42a26a67537", - "reference": "2ba804b4af205c060094c28cece0b42a26a67537", + "url": "https://api.github.com/repos/symfony/security/zipball/fa46e38ff4dea2d3949630efd33ed73e2ac0850a", + "reference": "fa46e38ff4dea2d3949630efd33ed73e2ac0850a", "shasum": "" }, "require": { @@ -3506,20 +3505,20 @@ ], "description": "Symfony Security Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-06-22T08:59:39+00:00" }, { "name": "symfony/security-bundle", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "8ac1bc3575a40eb57e0ed84f00bcb5964986945b" + "reference": "58c0db1915ab9c54c013d9336cace46f9e02cbb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/8ac1bc3575a40eb57e0ed84f00bcb5964986945b", - "reference": "8ac1bc3575a40eb57e0ed84f00bcb5964986945b", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/58c0db1915ab9c54c013d9336cace46f9e02cbb2", + "reference": "58c0db1915ab9c54c013d9336cace46f9e02cbb2", "shasum": "" }, "require": { @@ -3527,13 +3526,12 @@ "php": "^7.1.3", "symfony/dependency-injection": "^3.4.3|^4.0.3", "symfony/http-kernel": "^4.1", - "symfony/security": "~4.1" + "symfony/security": "^4.1.1" }, "conflict": { "symfony/console": "<3.4", "symfony/event-dispatcher": "<3.4", - "symfony/framework-bundle": "<=4.1-beta2", - "symfony/security": "4.1.0-beta1|4.1.0-beta2", + "symfony/framework-bundle": "<4.1.1", "symfony/var-dumper": "<3.4" }, "require-dev": { @@ -3587,20 +3585,20 @@ ], "description": "Symfony SecurityBundle", "homepage": "https://symfony.com", - "time": "2018-05-25T13:53:35+00:00" + "time": "2018-06-25T11:12:43+00:00" }, { "name": "symfony/serializer", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "db427d70438645789ffce6048d61b3992118a33a" + "reference": "2ddc6ec084eba809aec92bf723e007bc3a8345c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/db427d70438645789ffce6048d61b3992118a33a", - "reference": "db427d70438645789ffce6048d61b3992118a33a", + "url": "https://api.github.com/repos/symfony/serializer/zipball/2ddc6ec084eba809aec92bf723e007bc3a8345c0", + "reference": "2ddc6ec084eba809aec92bf723e007bc3a8345c0", "shasum": "" }, "require": { @@ -3667,7 +3665,7 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-06-22T08:59:39+00:00" }, { "name": "symfony/serializer-pack", @@ -3702,16 +3700,16 @@ }, { "name": "symfony/translation", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "16328f5b217cebc8dd4adfe4aeeaa8c377581f5a" + "reference": "b6d8164085ee0b6debcd1b7a131fd6f63bb04854" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/16328f5b217cebc8dd4adfe4aeeaa8c377581f5a", - "reference": "16328f5b217cebc8dd4adfe4aeeaa8c377581f5a", + "url": "https://api.github.com/repos/symfony/translation/zipball/b6d8164085ee0b6debcd1b7a131fd6f63bb04854", + "reference": "b6d8164085ee0b6debcd1b7a131fd6f63bb04854", "shasum": "" }, "require": { @@ -3767,20 +3765,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-06-22T08:59:39+00:00" }, { "name": "symfony/twig-bridge", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "b2e4c10df6517d1fdbb053326387f8e1779ad76e" + "reference": "4794c3cac1cd3c0104ec194ecd3ea82019d94c64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/b2e4c10df6517d1fdbb053326387f8e1779ad76e", - "reference": "b2e4c10df6517d1fdbb053326387f8e1779ad76e", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/4794c3cac1cd3c0104ec194ecd3ea82019d94c64", + "reference": "4794c3cac1cd3c0104ec194ecd3ea82019d94c64", "shasum": "" }, "require": { @@ -3857,44 +3855,44 @@ ], "description": "Symfony Twig Bridge", "homepage": "https://symfony.com", - "time": "2018-05-16T14:33:22+00:00" + "time": "2018-06-25T11:12:43+00:00" }, { "name": "symfony/twig-bundle", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "158bf522a8df74b0a322421bbde14a1647197007" + "reference": "04f9b5055ca8ea1cb2abc247149b0b4ae4bac6da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/158bf522a8df74b0a322421bbde14a1647197007", - "reference": "158bf522a8df74b0a322421bbde14a1647197007", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/04f9b5055ca8ea1cb2abc247149b0b4ae4bac6da", + "reference": "04f9b5055ca8ea1cb2abc247149b0b4ae4bac6da", "shasum": "" }, "require": { "php": "^7.1.3", "symfony/config": "~3.4|~4.0", - "symfony/http-foundation": "~3.4|~4.0", - "symfony/http-kernel": "~3.4|~4.0", + "symfony/http-foundation": "~4.1", + "symfony/http-kernel": "~4.1", "symfony/polyfill-ctype": "~1.8", "symfony/twig-bridge": "^3.4.3|^4.0.3", "twig/twig": "~1.34|~2.4" }, "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<3.4" + "symfony/dependency-injection": "<4.1", + "symfony/framework-bundle": "<4.1" }, "require-dev": { "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0", "symfony/asset": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", + "symfony/dependency-injection": "~4.1", "symfony/expression-language": "~3.4|~4.0", "symfony/finder": "~3.4|~4.0", "symfony/form": "~3.4|~4.0", - "symfony/framework-bundle": "~3.4|~4.0", + "symfony/framework-bundle": "~4.1", "symfony/routing": "~3.4|~4.0", "symfony/stopwatch": "~3.4|~4.0", "symfony/templating": "~3.4|~4.0", @@ -3931,20 +3929,20 @@ ], "description": "Symfony TwigBundle", "homepage": "https://symfony.com", - "time": "2018-05-16T14:41:07+00:00" + "time": "2018-06-25T11:12:43+00:00" }, { "name": "symfony/validator", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "f07bc14af6759710fb4639811a8240650d89786a" + "reference": "f2523bfd8dc5ff648aca55c0f2748674ca4661bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/f07bc14af6759710fb4639811a8240650d89786a", - "reference": "f07bc14af6759710fb4639811a8240650d89786a", + "url": "https://api.github.com/repos/symfony/validator/zipball/f2523bfd8dc5ff648aca55c0f2748674ca4661bb", + "reference": "f2523bfd8dc5ff648aca55c0f2748674ca4661bb", "shasum": "" }, "require": { @@ -4017,11 +4015,11 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-06-19T21:38:16+00:00" }, { "name": "symfony/web-server-bundle", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/web-server-bundle.git", @@ -4080,7 +4078,7 @@ }, { "name": "symfony/yaml", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -4423,7 +4421,7 @@ "packages-dev": [ { "name": "symfony/dotenv", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", @@ -4535,16 +4533,16 @@ }, { "name": "symfony/var-dumper", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "bc88ad53e825ebacc7b190bbd360781fce381c64" + "reference": "b2eebaec085d1f2cafbad7644733d494a3bbbc9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/bc88ad53e825ebacc7b190bbd360781fce381c64", - "reference": "bc88ad53e825ebacc7b190bbd360781fce381c64", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b2eebaec085d1f2cafbad7644733d494a3bbbc9b", + "reference": "b2eebaec085d1f2cafbad7644733d494a3bbbc9b", "shasum": "" }, "require": { @@ -4606,20 +4604,20 @@ "debug", "dump" ], - "time": "2018-04-29T07:56:09+00:00" + "time": "2018-06-23T12:23:56+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "8b3914ac388b2c560d4dc162c8136d136245718b" + "reference": "0043c661d7dda6cc07b28345832912e548e204b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/8b3914ac388b2c560d4dc162c8136d136245718b", - "reference": "8b3914ac388b2c560d4dc162c8136d136245718b", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/0043c661d7dda6cc07b28345832912e548e204b6", + "reference": "0043c661d7dda6cc07b28345832912e548e204b6", "shasum": "" }, "require": { @@ -4672,7 +4670,7 @@ ], "description": "Symfony WebProfilerBundle", "homepage": "https://symfony.com", - "time": "2018-05-23T13:33:23+00:00" + "time": "2018-06-12T12:15:08+00:00" } ], "aliases": [], From 857a6c84c44123a8cedd965749c7385e1ae13e58 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Tue, 26 Jun 2018 20:48:52 +0300 Subject: [PATCH 18/24] small config changes. --- config/packages/security.yaml | 2 +- config/routes.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 3d84a75..c919848 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -45,7 +45,7 @@ security: # 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: ^/api/v1/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/api/v1/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api/, roles: ROLE_USER } - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /login, roles: IS_AUTHENTICATED_ANONYMOUSLY } diff --git a/config/routes.yaml b/config/routes.yaml index 20ddb7f..774bbff 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -31,7 +31,7 @@ user_logout: path: /logout # API -api_login: +api_v1_login: path: /api/v1/login controller: App\Api\V1\Controller\SecurityController::login defaults: From 84f2d5ece9a46ba56c5ac5ffb344cff012b20986 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Tue, 26 Jun 2018 22:09:46 +0300 Subject: [PATCH 19/24] small config changes. --- src/Security/ApiTokenAuthenticator.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Security/ApiTokenAuthenticator.php b/src/Security/ApiTokenAuthenticator.php index 7610bd4..99561f0 100644 --- a/src/Security/ApiTokenAuthenticator.php +++ b/src/Security/ApiTokenAuthenticator.php @@ -26,7 +26,9 @@ class ApiTokenAuthenticator implements SimplePreAuthenticatorInterface, Authenti public function createToken(Request $request, $providerKey) { if (!($tokenKey = $request->headers->get(self::TOKEN_HEADER))) { - throw new BadCredentialsException(sprintf('\'%s\' is invalid or not defined', self::TOKEN_HEADER)); + // Throwing exception here will break anonymous authentication for login method + //throw new BadCredentialsException(sprintf('\'%s\' is invalid or not defined', self::TOKEN_HEADER)); + return null; } return new PreAuthenticatedToken( From b35da23e8e9a45bde0eb9c0bd81b681d313e6722 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Wed, 27 Jun 2018 01:28:42 +0300 Subject: [PATCH 20/24] #2 API token authentication implemented. Login method implemented. Some refactoring. ApiTokenRepository::findUserByTokenKey() query bug fix. --- .../V1/Controller/AbstractApiController.php | 2 +- src/Api/V1/Controller/SecurityController.php | 36 +++++++++++++++++-- src/Api/V1/DTO/ApiResponse.php | 8 ++--- src/Api/V1/DTO/ListPage.php | 10 +++--- src/Entity/ApiToken.php | 3 ++ src/Repository/ApiTokenRepository.php | 7 ++-- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/Api/V1/Controller/AbstractApiController.php b/src/Api/V1/Controller/AbstractApiController.php index fb0a90c..47f5548 100644 --- a/src/Api/V1/Controller/AbstractApiController.php +++ b/src/Api/V1/Controller/AbstractApiController.php @@ -8,7 +8,7 @@ use Symfony\Component\HttpFoundation\{JsonResponse, Response}; abstract class AbstractApiController extends Controller { - protected const DEFAULT_SERIALIZER_GROUPS = ['api_v1']; + protected const DEFAULT_SERIALIZER_GROUPS = ['api']; protected function createJsonResponse($data, array $groups = [], int $code = Response::HTTP_OK, string $message = null, string $status = ''): JsonResponse { diff --git a/src/Api/V1/Controller/SecurityController.php b/src/Api/V1/Controller/SecurityController.php index 46c3a6f..7f123db 100644 --- a/src/Api/V1/Controller/SecurityController.php +++ b/src/Api/V1/Controller/SecurityController.php @@ -2,12 +2,42 @@ namespace App\Api\V1\Controller; +use App\Entity\{ApiToken, User}; +use App\Repository\{ApiTokenRepository, UserRepository}; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\{JsonResponse, Request}; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; class SecurityController extends AbstractApiController { - public function login(Request $request): JsonResponse - { - // @todo implement login procedure + public function login( + Request $request, + EntityManagerInterface $em, + UserRepository $userRepo, + ApiTokenRepository $tokenRepo, + UserPasswordEncoderInterface $passwordEncoder + ): JsonResponse { + $username = $request->request->get('username'); + $password = $request->request->get('password'); + + /** @var User $user */ + if (null === $user = $userRepo->findOneBy(['username' => $username])) { + return $this->createJsonResponse(null, [], JsonResponse::HTTP_UNAUTHORIZED, 'User not found'); + } + + if (!$passwordEncoder->isPasswordValid($user, $password)) { + return $this->createJsonResponse(null, [], JsonResponse::HTTP_UNAUTHORIZED, 'Invalid password'); + } + + $apiToken = new ApiToken($user); + $tokenRepo->add($apiToken); + + try { + $em->flush(); + } catch (\Exception $ex) { + return $this->createJsonResponse(null, [], JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Token persisting error'); + } + + return $this->createJsonResponse($apiToken->getKey()); } } \ No newline at end of file diff --git a/src/Api/V1/DTO/ApiResponse.php b/src/Api/V1/DTO/ApiResponse.php index b31a6ed..92dcd7f 100644 --- a/src/Api/V1/DTO/ApiResponse.php +++ b/src/Api/V1/DTO/ApiResponse.php @@ -15,28 +15,28 @@ class ApiResponse /** * @var int HTTP response status code * - * @Groups({"api_v1"}) + * @Groups({"api"}) */ private $code; /** * @var string Status text: 'success' (1xx-3xx), 'error' (4xx), 'fail' (5xx) or 'unknown' * - * @Groups({"api_v1"}) + * @Groups({"api"}) */ private $status; /** * @var string|null Used for 'fail' and 'error' * - * @Groups({"api_v1"}) + * @Groups({"api"}) */ private $message; /** * @var string|\object|array|null Response body. In case of 'error' or 'fail' contains cause or exception name. * - * @Groups({"api_v1"}) + * @Groups({"api"}) */ private $data; diff --git a/src/Api/V1/DTO/ListPage.php b/src/Api/V1/DTO/ListPage.php index 6c81cfd..3ddd47b 100644 --- a/src/Api/V1/DTO/ListPage.php +++ b/src/Api/V1/DTO/ListPage.php @@ -10,35 +10,35 @@ class ListPage /** * @var int * - * @Serializer\Groups({"api_v1"}) + * @Serializer\Groups({"api"}) */ private $numberOfPages; /** * @var int * - * @Serializer\Groups({"api_v1"}) + * @Serializer\Groups({"api"}) */ private $currentPage; /** * @var int * - * @Serializer\Groups({"api_v1"}) + * @Serializer\Groups({"api"}) */ private $numberOfResults; /** * @var int * - * @Serializer\Groups({"api_v1"}) + * @Serializer\Groups({"api"}) */ private $maxPerPage; /** * @var \Traversable * - * @Serializer\Groups({"api_v1"}) + * @Serializer\Groups({"api"}) */ protected $items; diff --git a/src/Entity/ApiToken.php b/src/Entity/ApiToken.php index 61ef9d1..eb58869 100644 --- a/src/Entity/ApiToken.php +++ b/src/Entity/ApiToken.php @@ -3,6 +3,7 @@ namespace App\Entity; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation as Serializer; /** * @ORM\Table(name="api_tokens", schema="users") @@ -21,6 +22,8 @@ class ApiToken /** * @var string * + * @Serializer\Groups({"api", "api_v1_login"}) + * * @ORM\Id() * @ORM\Column(name="key", type="string", length=32) */ diff --git a/src/Repository/ApiTokenRepository.php b/src/Repository/ApiTokenRepository.php index 0a7847b..523be7c 100644 --- a/src/Repository/ApiTokenRepository.php +++ b/src/Repository/ApiTokenRepository.php @@ -4,6 +4,7 @@ namespace App\Repository; use App\Entity\{ApiToken, User}; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query\Expr\Join; use Symfony\Bridge\Doctrine\RegistryInterface; class ApiTokenRepository extends ServiceEntityRepository @@ -20,9 +21,11 @@ class ApiTokenRepository extends ServiceEntityRepository public function findUserByTokenKey(string $tokenKey): ?User { - $qb = $this->createQueryBuilder('at'); + $qb = $this->getEntityManager()->createQueryBuilder(); $qb - ->select('at.user') + ->select('u') + ->from(User::class, 'u') + ->innerJoin(ApiToken::class, 'at', Join::WITH, 'at.user = u') ->where('at.key = :tokenKey') ->setParameter('tokenKey', $tokenKey) ; From 0dd74177e3450ddccbc2d5f58c8fe3f41ff1ff73 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Wed, 27 Jun 2018 01:40:43 +0300 Subject: [PATCH 21/24] Unique index on Invite::$code. --- src/Entity/Invite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Invite.php b/src/Entity/Invite.php index 9b94836..14f6870 100644 --- a/src/Entity/Invite.php +++ b/src/Entity/Invite.php @@ -30,7 +30,7 @@ class Invite /** * @var string * - * @ORM\Column(name="code", type="string", length=32) + * @ORM\Column(name="code", type="string", length=32, unique=true) */ private $code; From 5136a202410344dd338197bac194d0bd8063fb34 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Wed, 27 Jun 2018 01:41:19 +0300 Subject: [PATCH 22/24] Initial migration for PostgreSQL database. 'users' schema with tables added. --- src/Migrations/Version20180626223216.php | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/Migrations/Version20180626223216.php diff --git a/src/Migrations/Version20180626223216.php b/src/Migrations/Version20180626223216.php new file mode 100644 index 0000000..c2f6505 --- /dev/null +++ b/src/Migrations/Version20180626223216.php @@ -0,0 +1,50 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE SCHEMA users'); + $this->addSql('CREATE SEQUENCE users.users_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE users.invites_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE users.users (id INT NOT NULL, username VARCHAR(25) NOT NULL, password TEXT NOT NULL, email VARCHAR(254) NOT NULL, roles JSON NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_338ADFC4F85E0677 ON users.users (username)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_338ADFC4E7927C74 ON users.users (email)'); + $this->addSql('CREATE TABLE users.invites (id INT NOT NULL, user_id INT NOT NULL, used_by_id INT DEFAULT NULL, code VARCHAR(32) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_93848FA877153098 ON users.invites (code)'); + $this->addSql('CREATE INDEX IDX_93848FA8A76ED395 ON users.invites (user_id)'); + $this->addSql('CREATE INDEX IDX_93848FA84C2B72A8 ON users.invites (used_by_id)'); + $this->addSql('CREATE TABLE users.api_tokens (key VARCHAR(32) NOT NULL, user_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(key))'); + $this->addSql('CREATE INDEX IDX_C46A74A9A76ED395 ON users.api_tokens (user_id)'); + $this->addSql('ALTER TABLE users.invites ADD CONSTRAINT FK_93848FA8A76ED395 FOREIGN KEY (user_id) REFERENCES users.users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users.invites ADD CONSTRAINT FK_93848FA84C2B72A8 FOREIGN KEY (used_by_id) REFERENCES users.users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users.api_tokens ADD CONSTRAINT FK_C46A74A9A76ED395 FOREIGN KEY (user_id) REFERENCES users.users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE users.invites DROP CONSTRAINT FK_93848FA8A76ED395'); + $this->addSql('ALTER TABLE users.invites DROP CONSTRAINT FK_93848FA84C2B72A8'); + $this->addSql('ALTER TABLE users.api_tokens DROP CONSTRAINT FK_C46A74A9A76ED395'); + $this->addSql('DROP SEQUENCE users.users_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE users.invites_id_seq CASCADE'); + $this->addSql('DROP TABLE users.users'); + $this->addSql('DROP TABLE users.invites'); + $this->addSql('DROP TABLE users.api_tokens'); + $this->addSql('DROP SCHEMA users'); + } +} From 61405ead60b635397af7491b0af8afb50d45ca81 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Wed, 27 Jun 2018 02:02:32 +0300 Subject: [PATCH 23/24] AuthenticatedApiToken implemented for per-request ApiToken::$key value storage. --- src/Security/ApiTokenAuthenticator.php | 3 ++- src/Security/Token/AuthenticatedApiToken.php | 25 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/Security/Token/AuthenticatedApiToken.php diff --git a/src/Security/ApiTokenAuthenticator.php b/src/Security/ApiTokenAuthenticator.php index 99561f0..2d1adb1 100644 --- a/src/Security/ApiTokenAuthenticator.php +++ b/src/Security/ApiTokenAuthenticator.php @@ -3,6 +3,7 @@ namespace App\Security; use App\Api\V1\DTO\ApiResponse; +use App\Security\Token\AuthenticatedApiToken; use Symfony\Component\HttpFoundation\{JsonResponse, Request}; use Symfony\Component\Security\Core\Authentication\Token\{PreAuthenticatedToken, TokenInterface}; use Symfony\Component\Security\Core\Exception\{AuthenticationException, BadCredentialsException, CustomUserMessageAuthenticationException}; @@ -59,7 +60,7 @@ class ApiTokenAuthenticator implements SimplePreAuthenticatorInterface, Authenti )); } - return new PreAuthenticatedToken( + return new AuthenticatedApiToken( $user, $apiTokenKey, $providerKey, diff --git a/src/Security/Token/AuthenticatedApiToken.php b/src/Security/Token/AuthenticatedApiToken.php new file mode 100644 index 0000000..f7d1336 --- /dev/null +++ b/src/Security/Token/AuthenticatedApiToken.php @@ -0,0 +1,25 @@ +tokenKey = $credentials; + } + + public function getTokenKey(): ?string + { + return $this->tokenKey; + } +} \ No newline at end of file From 6dec613b619ffa1913efee3b760f4abd48029945 Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Wed, 27 Jun 2018 02:15:39 +0300 Subject: [PATCH 24/24] #2 API logout implemented. --- config/routes.yaml | 9 +++++++ src/Api/V1/Controller/SecurityController.php | 28 ++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/config/routes.yaml b/config/routes.yaml index 774bbff..aa67fd3 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -40,6 +40,15 @@ api_v1_login: method: POST _format: json +api_v1_logout: + path: /api/v1/logout + controller: App\Api\V1\Controller\SecurityController::logout + defaults: + _format: json + requirements: + method: GET + _format: json + api_v1_torrents: path: /api/v1/torrents controller: App\Api\V1\Controller\TorrentController::search diff --git a/src/Api/V1/Controller/SecurityController.php b/src/Api/V1/Controller/SecurityController.php index 7f123db..58f7e85 100644 --- a/src/Api/V1/Controller/SecurityController.php +++ b/src/Api/V1/Controller/SecurityController.php @@ -4,8 +4,10 @@ namespace App\Api\V1\Controller; use App\Entity\{ApiToken, User}; use App\Repository\{ApiTokenRepository, UserRepository}; +use App\Security\Token\AuthenticatedApiToken; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\{JsonResponse, Request}; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; class SecurityController extends AbstractApiController @@ -40,4 +42,30 @@ class SecurityController extends AbstractApiController return $this->createJsonResponse($apiToken->getKey()); } + + public function logout(TokenStorageInterface $tokenStorage, ApiTokenRepository $apiTokenRepo, EntityManagerInterface $em): JsonResponse + { + $token = $tokenStorage->getToken(); + + if (!$token instanceof AuthenticatedApiToken) { + return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Invalid session token type retrieved.'); + } + if (null === $apiTokenKey = $token->getTokenKey()) { + return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Can\'t retrieve token key from session.'); + } + + if (null === $apiToken = $apiTokenRepo->findOneBy(['key' => $apiTokenKey])) { + return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'API token with such key not found in the database.'); + } + + $em->remove($apiToken); + + try { + $em->flush(); + } catch (\Exception $ex) { + return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'API token deauthentication failure.'); + } + + return $this->createJsonResponse(null,[],JsonResponse::HTTP_OK, 'Successfully logged out.'); + } } \ No newline at end of file
Name
{{ file.path }}