├── LICENSE
├── bin
├── console
└── phpunit
├── composer.json
├── composer.lock
├── config
├── bootstrap.php
├── bundles.php
├── packages
│ ├── api_platform.yaml
│ ├── api_projections.yaml
│ ├── cache.yaml
│ ├── dev
│ │ ├── debug.yaml
│ │ ├── lexik_jwt_authentication.yaml
│ │ ├── monolog.yaml
│ │ ├── routing.yaml
│ │ └── web_profiler.yaml
│ ├── doctrine.yaml
│ ├── doctrine_migrations.yaml
│ ├── elasticsearch.yaml
│ ├── framework.yaml
│ ├── httplug.yaml
│ ├── hwi_oauth.yaml
│ ├── lexik_jwt_authentication.yaml
│ ├── mailer.yaml
│ ├── messenger.yaml
│ ├── msgphp_eav.php
│ ├── msgphp_user.php
│ ├── nelmio_cors.yaml
│ ├── prod
│ │ ├── doctrine.yaml
│ │ ├── monolog.yaml
│ │ └── routing.yaml
│ ├── ramsey_uuid_doctrine.yaml
│ ├── routing.yaml
│ ├── security.yaml
│ ├── sensio_framework_extra.yaml
│ ├── test
│ │ ├── framework.yaml
│ │ ├── monolog.yaml
│ │ ├── routing.yaml
│ │ ├── twig.yaml
│ │ ├── validator.yaml
│ │ └── web_profiler.yaml
│ ├── translation.yaml
│ ├── twig.yaml
│ └── validator.yaml
├── routes.yaml
├── routes
│ ├── annotations.yaml
│ ├── api_platform.yaml
│ ├── dev
│ │ ├── framework.yaml
│ │ └── web_profiler.yaml
│ └── hwi_oauth_routing.yaml
├── services.yaml
└── services_dev.yaml
├── phpunit.xml.dist
├── psalm.xml
├── public
└── index.php
├── src
├── Api
│ ├── DocumentIdentity.php
│ ├── Endpoint
│ │ ├── CreateUserEndpoint.php
│ │ └── DeleteUserEndpoint.php
│ ├── Projection
│ │ └── UserProjection.php
│ └── Serializer
│ │ └── UserNormalizer.php
├── Console
│ ├── ClassContextElementFactory.php
│ └── Command
│ │ ├── CreateOneTimeLoginTokenCommand.php
│ │ └── InviteUserCommand.php
├── Controller
│ ├── ConfirmEmailController.php
│ ├── ConfirmRegistrationController.php
│ ├── ForgotPasswordController.php
│ ├── HomeController.php
│ ├── LoginController.php
│ ├── ProfileController.php
│ ├── RegisterController.php
│ └── ResetPasswordController.php
├── DataFixtures
│ └── AppFixtures.php
├── Entity
│ ├── Attribute.php
│ ├── AttributeValue.php
│ ├── OneTimeLoginToken.php
│ ├── PremiumUser.php
│ ├── Role.php
│ ├── User.php
│ ├── UserAttributeValue.php
│ ├── UserEmail.php
│ ├── UserInvitation.php
│ ├── UserRole.php
│ └── Username.php
├── EventSubscriber
│ ├── EnableConfirmedUser.php
│ ├── InvalidateUserInvitation.php
│ ├── SendEmailConfirmationUrl.php
│ ├── SendPasswordResetUrl.php
│ ├── SendRegistrationConfirmationUrl.php
│ └── SynchronizeApi.php
├── Form
│ ├── AddEmailType.php
│ ├── ChangePasswordType.php
│ ├── ForgotPasswordType.php
│ ├── LoginType.php
│ ├── OneTimeLoginType.php
│ ├── RegisterType.php
│ └── ResetPasswordType.php
├── Http
│ ├── Respond.php
│ ├── RespondBadRequest.php
│ ├── RespondEmpty.php
│ ├── RespondFile.php
│ ├── RespondJson.php
│ ├── RespondNotFound.php
│ ├── RespondRaw.php
│ ├── RespondRawJson.php
│ ├── RespondRedirect.php
│ ├── RespondRouteRedirect.php
│ ├── RespondStream.php
│ ├── RespondTemplate.php
│ └── Responder.php
├── Kernel.php
└── Security
│ ├── JwtTokenSubscriber.php
│ ├── OauthUserProvider.php
│ ├── OneTimeLoginAuthenticator.php
│ ├── PasswordConfirmation.php
│ ├── RoleProvider.php
│ └── UserChecker.php
├── symfony.lock
├── templates
├── base.html.twig
├── email.html.twig
├── email.txt.twig
├── home.html.twig
├── partials
│ ├── flash-messages.html.twig
│ └── oauth
│ │ └── login_entrypoint.html.twig
├── password_confirmation.html.twig
└── user
│ ├── email
│ ├── confirm_email.html.twig
│ ├── confirm_email.txt.twig
│ ├── confirm_registration.html.twig
│ ├── confirm_registration.txt.twig
│ ├── invited.html.twig
│ ├── invited.txt.twig
│ ├── reset_password.html.twig
│ └── reset_password.txt.twig
│ ├── forgot_password.html.twig
│ ├── login.html.twig
│ ├── profile.html.twig
│ ├── register.html.twig
│ └── reset_password.html.twig
├── tests
└── bootstrap.php
└── translations
└── messages+intl-icu.en.xlf
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Roland Franssen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getParameterOption(['--env', '-e'], null, true)) {
23 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
24 | }
25 |
26 | if ($input->hasParameterOption('--no-debug', true)) {
27 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
28 | }
29 |
30 | require dirname(__DIR__).'/config/bootstrap.php';
31 |
32 | if ($_SERVER['APP_DEBUG']) {
33 | umask(0000);
34 |
35 | if (class_exists(Debug::class)) {
36 | Debug::enable();
37 | }
38 | }
39 |
40 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
41 | $application = new Application($kernel);
42 | $application->run($input);
43 |
--------------------------------------------------------------------------------
/bin/phpunit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | =1.2)
11 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
12 | foreach ($env as $k => $v) {
13 | $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v);
14 | }
15 | } elseif (!class_exists(Dotenv::class)) {
16 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
17 | } else {
18 | // load all the .env files
19 | (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
20 | }
21 |
22 | $_SERVER += $_ENV;
23 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
24 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
25 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';
26 |
--------------------------------------------------------------------------------
/config/bundles.php:
--------------------------------------------------------------------------------
1 | ['all' => true],
7 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
8 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
9 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
10 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
11 | ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
12 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
13 | HWI\Bundle\OAuthBundle\HWIOAuthBundle::class => ['all' => true],
14 | Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
15 | MsgPhp\EavBundle\MsgPhpEavBundle::class => ['all' => true],
16 | MsgPhp\UserBundle\MsgPhpUserBundle::class => ['all' => true],
17 | Http\HttplugBundle\HttplugBundle::class => ['all' => true],
18 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
19 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
20 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
21 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
22 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
23 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
24 | ];
25 |
--------------------------------------------------------------------------------
/config/packages/api_platform.yaml:
--------------------------------------------------------------------------------
1 | api_platform:
2 | title: MsgPHP demo API
3 | version: 1.0.0
4 | name_converter: api_name_converter
5 | path_segment_name_generator: api_platform.path_segment_name_generator.dash
6 | mapping:
7 | paths: ['%kernel.project_dir%/src/Api/Projection']
8 | elasticsearch:
9 | hosts: ['%env(ELASTICSEARCH_HOST)%']
10 | mapping:
11 | App\Api\Projection\UserProjection: { index: '%api_projections.index_prefix%user' }
12 | graphql:
13 | enabled: true
14 | graphiql:
15 | enabled: true
16 | http_cache:
17 | vary: [Accept, Authorization]
18 | patch_formats:
19 | json: ['application/merge-patch+json']
20 | swagger:
21 | versions: [3]
22 | api_keys:
23 | - { name: Authorization, type: header }
24 |
25 | services:
26 | api_name_converter:
27 | class: Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter
28 |
--------------------------------------------------------------------------------
/config/packages/api_projections.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | api_projections.index_prefix: 'api_%kernel.environment%-'
3 | api_projections.class_lookup:
4 | App\Entity\User: user
5 | api_projections.mappings:
6 | user:
7 | id: keyword
8 | user_id: text
9 | email: text
10 | # https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html#create-index-settings
11 | api_projections.settings:
12 | '*': {}
13 | user: {}
14 |
15 | services:
16 | _defaults:
17 | autowire: true
18 | autoconfigure: true
19 | public: false
20 |
21 | # Type resolver
22 | MsgPhp\Domain\Projection\ProjectionTypeResolver:
23 | arguments:
24 | '$classLookup': '%api_projections.class_lookup%'
25 |
26 | # Type registry
27 | MsgPhp\Domain\Infrastructure\Elasticsearch\ProjectionTypeRegistry:
28 | arguments:
29 | '$prefix': '%api_projections.index_prefix%'
30 | '$mappings': '%api_projections.mappings%'
31 | '$settings': '%api_projections.settings%'
32 | MsgPhp\Domain\Projection\ProjectionTypeRegistry: '@MsgPhp\Domain\Infrastructure\Elasticsearch\ProjectionTypeRegistry'
33 |
34 | # Document transformer
35 | MsgPhp\Domain\Infrastructure\Serializer\ProjectionDocumentTransformer: ~
36 |
37 | # Document provider
38 | MsgPhp\Domain\Projection\ProjectionDocumentProvider:
39 | arguments:
40 | '$dataProviders':
41 | - ['@MsgPhp\User\Repository\UserRepository', findAll]
42 | '$transformer': '@MsgPhp\Domain\Infrastructure\Serializer\ProjectionDocumentTransformer'
43 | '$typeResolver': '@MsgPhp\Domain\Projection\ProjectionTypeResolver'
44 |
45 | # Repository
46 | MsgPhp\Domain\Infrastructure\Elasticsearch\ProjectionRepository:
47 | arguments:
48 | '$prefix': '%api_projections.index_prefix%'
49 | MsgPhp\Domain\Projection\ProjectionRepository: '@MsgPhp\Domain\Infrastructure\Elasticsearch\ProjectionRepository'
50 |
51 | # Synchronization
52 | MsgPhp\Domain\Projection\ProjectionSynchronization:
53 | arguments:
54 | '$documentProvider': '@MsgPhp\Domain\Projection\ProjectionDocumentProvider'
55 |
56 | # Console
57 | MsgPhp\Domain\Infrastructure\Console\Command\SynchronizeProjectionsCommand: ~
58 |
59 | # Messenger
60 | MsgPhp\Domain\Projection\Command\Handler\DeleteProjectionHandler:
61 | tags:
62 | - { name: messenger.message_handler, bus: command_bus }
63 | MsgPhp\Domain\Projection\Command\Handler\SaveProjectionHandler:
64 | tags:
65 | - { name: messenger.message_handler, bus: command_bus }
66 |
--------------------------------------------------------------------------------
/config/packages/cache.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | cache:
3 | # Unique name of your app: used to compute stable namespaces for cache keys.
4 | #prefix_seed: your_vendor_name/app_name
5 |
6 | # The "app" cache stores to the filesystem by default.
7 | # The data in this cache should persist between deploys.
8 | # Other options include:
9 |
10 | # Redis
11 | #app: cache.adapter.redis
12 | #default_redis_provider: redis://localhost
13 |
14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
15 | #app: cache.adapter.apcu
16 |
17 | # Namespaced pools use the above "app" backend by default
18 | #pools:
19 | #my.dedicated.cache: null
20 |
--------------------------------------------------------------------------------
/config/packages/dev/debug.yaml:
--------------------------------------------------------------------------------
1 | debug:
2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
3 | # See the "server:dump" command to start a new server.
4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
5 |
--------------------------------------------------------------------------------
/config/packages/dev/lexik_jwt_authentication.yaml:
--------------------------------------------------------------------------------
1 | lexik_jwt_authentication:
2 | token_extractors:
3 | query_parameter: { name: jwt, enabled: true }
4 |
--------------------------------------------------------------------------------
/config/packages/dev/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | handlers:
3 | main:
4 | type: stream
5 | path: "%kernel.logs_dir%/%kernel.environment%.log"
6 | level: debug
7 | channels: ["!event"]
8 | # uncomment to get logging in your browser
9 | # you may have to allow bigger header sizes in your Web server configuration
10 | #firephp:
11 | # type: firephp
12 | # level: info
13 | #chromephp:
14 | # type: chromephp
15 | # level: info
16 | console:
17 | type: console
18 | process_psr_3_messages: false
19 | channels: ["!event", "!doctrine", "!console"]
20 |
--------------------------------------------------------------------------------
/config/packages/dev/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | strict_requirements: true
4 |
--------------------------------------------------------------------------------
/config/packages/dev/web_profiler.yaml:
--------------------------------------------------------------------------------
1 | web_profiler:
2 | toolbar: true
3 | intercept_redirects: false
4 |
5 | framework:
6 | profiler: { only_exceptions: false }
7 |
--------------------------------------------------------------------------------
/config/packages/doctrine.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | msgphp.doctrine.mapping_config:
3 | key_max_length: 191
4 |
5 | doctrine:
6 | dbal:
7 | url: '%env(resolve:DATABASE_URL)%'
8 |
9 | # IMPORTANT: You MUST configure your server version,
10 | # either here or in the DATABASE_URL env var (see .env file)
11 | #server_version: '5.7'
12 | orm:
13 | auto_generate_proxy_classes: true
14 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
15 | auto_mapping: true
16 | mappings:
17 | App:
18 | is_bundle: false
19 | type: annotation
20 | dir: '%kernel.project_dir%/src/Entity'
21 | prefix: 'App\Entity'
22 | alias: App
23 |
--------------------------------------------------------------------------------
/config/packages/doctrine_migrations.yaml:
--------------------------------------------------------------------------------
1 | doctrine_migrations:
2 | dir_name: '%kernel.project_dir%/src/Migrations'
3 | # namespace is arbitrary but should be different from App\Migrations
4 | # as migrations classes should NOT be autoloaded
5 | namespace: DoctrineMigrations
6 |
--------------------------------------------------------------------------------
/config/packages/elasticsearch.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | _defaults:
3 | public: false
4 |
5 | # Client
6 | Elasticsearch\ClientBuilder:
7 | factory: Elasticsearch\ClientBuilder::create
8 | calls:
9 | - [setHosts, [['%env(ELASTICSEARCH_HOST)%']]]
10 |
11 | Elasticsearch\Client:
12 | factory: ['@Elasticsearch\ClientBuilder', build]
13 |
--------------------------------------------------------------------------------
/config/packages/framework.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | secret: '%env(APP_SECRET)%'
3 | #csrf_protection: true
4 | #http_method_override: true
5 |
6 | # Enables session support. Note that the session will ONLY be started if you read or write from it.
7 | # Remove or comment this section to explicitly disable session support.
8 | session:
9 | handler_id: null
10 | cookie_secure: auto
11 | cookie_samesite: lax
12 |
13 | #esi: true
14 | #fragments: true
15 | php_errors:
16 | log: true
17 |
--------------------------------------------------------------------------------
/config/packages/httplug.yaml:
--------------------------------------------------------------------------------
1 | httplug:
2 | plugins:
3 | retry:
4 | retry: 1
5 |
6 | discovery:
7 | client: 'auto'
8 |
9 | clients:
10 | app:
11 | http_methods_client: true
12 | plugins:
13 | - 'httplug.plugin.content_length'
14 | - 'httplug.plugin.redirect'
15 |
--------------------------------------------------------------------------------
/config/packages/hwi_oauth.yaml:
--------------------------------------------------------------------------------
1 | hwi_oauth:
2 | # list of names of the firewalls in which this bundle is active, this setting MUST be set
3 | firewall_names: [main]
4 |
5 | # https://github.com/hwi/HWIOAuthBundle/blob/master/Resources/doc/2-configuring_resource_owners.md
6 | resource_owners:
7 | facebook:
8 | type: facebook
9 | client_id: '%env(FB_ID)%'
10 | client_secret: '%env(FB_SECRET)%'
11 | scope: "email"
12 | options:
13 | display: popup
14 | csrf: true
15 | google:
16 | type: google
17 | client_id: '%env(GOOGLE_ID)%'
18 | client_secret: '%env(GOOGLE_SECRET)%'
19 | scope: email
20 | options:
21 | csrf: true
22 |
--------------------------------------------------------------------------------
/config/packages/lexik_jwt_authentication.yaml:
--------------------------------------------------------------------------------
1 | lexik_jwt_authentication:
2 | secret_key: '%env(resolve:JWT_SECRET_KEY)%'
3 | public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
4 | pass_phrase: '%env(JWT_PASSPHRASE)%'
5 |
--------------------------------------------------------------------------------
/config/packages/mailer.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | mailer:
3 | dsn: '%env(MAILER_DSN)%'
4 |
--------------------------------------------------------------------------------
/config/packages/messenger.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | messenger:
3 | # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
4 | # failure_transport: failed
5 |
6 | transports:
7 | # https://symfony.com/doc/current/messenger.html#transport-configuration
8 | # async: '%env(MESSENGER_TRANSPORT_DSN)%'
9 | # failed: 'doctrine://default?queue_name=failed'
10 | # sync: 'sync://'
11 |
12 | routing:
13 | # Route your messages to the transports
14 | # 'App\Message\YourMessage': async
15 |
16 | default_bus: command_bus
17 | buses:
18 | command_bus: ~
19 | event_bus:
20 | default_middleware: allow_no_handlers
21 |
22 | services:
23 | msgphp.messenger.command_bus: '@command_bus'
24 | msgphp.messenger.event_bus: '@event_bus'
25 |
--------------------------------------------------------------------------------
/config/packages/msgphp_eav.php:
--------------------------------------------------------------------------------
1 | extension('msgphp_eav', [
11 | 'class_mapping' => [
12 | Attribute::class => \App\Entity\Attribute::class,
13 | AttributeValue::class => \App\Entity\AttributeValue::class,
14 | ],
15 | 'default_id_type' => 'uuid',
16 | ]);
17 | };
18 |
--------------------------------------------------------------------------------
/config/packages/msgphp_user.php:
--------------------------------------------------------------------------------
1 | extension('msgphp_user', [
17 | 'class_mapping' => [
18 | Role::class => \App\Entity\Role::class,
19 | User::class => \App\Entity\User::class,
20 | UserAttributeValue::class => \App\Entity\UserAttributeValue::class,
21 | UserEmail::class => \App\Entity\UserEmail::class,
22 | Username::class => \App\Entity\Username::class,
23 | UserRole::class => \App\Entity\UserRole::class,
24 | ],
25 | 'default_id_type' => 'uuid',
26 | 'username_lookup' => [
27 | ['target' => \App\Entity\UserEmail::class, 'field' => 'email', 'mapped_by' => 'user'],
28 | ],
29 | 'role_providers' => [
30 | 'default' => [RoleProvider::ROLE_USER],
31 | UserRoleProvider::class,
32 | RoleProvider::class,
33 | ],
34 | ]);
35 | };
36 |
--------------------------------------------------------------------------------
/config/packages/nelmio_cors.yaml:
--------------------------------------------------------------------------------
1 | nelmio_cors:
2 | defaults:
3 | origin_regex: true
4 | allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
5 | allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
6 | allow_headers: ['Content-Type', 'Authorization']
7 | expose_headers: ['Link']
8 | max_age: 3600
9 | paths:
10 | '^/': null
11 |
--------------------------------------------------------------------------------
/config/packages/prod/doctrine.yaml:
--------------------------------------------------------------------------------
1 | doctrine:
2 | orm:
3 | auto_generate_proxy_classes: false
4 | metadata_cache_driver:
5 | type: pool
6 | pool: doctrine.system_cache_pool
7 | query_cache_driver:
8 | type: pool
9 | pool: doctrine.system_cache_pool
10 | result_cache_driver:
11 | type: pool
12 | pool: doctrine.result_cache_pool
13 |
14 | framework:
15 | cache:
16 | pools:
17 | doctrine.result_cache_pool:
18 | adapter: cache.app
19 | doctrine.system_cache_pool:
20 | adapter: cache.system
21 |
--------------------------------------------------------------------------------
/config/packages/prod/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | handlers:
3 | main:
4 | type: fingers_crossed
5 | action_level: error
6 | handler: nested
7 | excluded_http_codes: [404, 405]
8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks
9 | nested:
10 | type: stream
11 | path: "%kernel.logs_dir%/%kernel.environment%.log"
12 | level: debug
13 | console:
14 | type: console
15 | process_psr_3_messages: false
16 | channels: ["!event", "!doctrine"]
17 | deprecation:
18 | type: stream
19 | path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
20 | deprecation_filter:
21 | type: filter
22 | handler: deprecation
23 | max_level: info
24 | channels: ["php"]
25 |
--------------------------------------------------------------------------------
/config/packages/prod/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | strict_requirements: null
4 |
--------------------------------------------------------------------------------
/config/packages/ramsey_uuid_doctrine.yaml:
--------------------------------------------------------------------------------
1 | doctrine:
2 | dbal:
3 | types:
4 | uuid: 'Ramsey\Uuid\Doctrine\UuidType'
5 |
--------------------------------------------------------------------------------
/config/packages/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | utf8: true
4 |
--------------------------------------------------------------------------------
/config/packages/security.yaml:
--------------------------------------------------------------------------------
1 | security:
2 | encoders:
3 | MsgPhp\User\Infrastructure\Security\UserIdentity: auto
4 |
5 | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
6 | providers:
7 | msgphp_user: { id: MsgPhp\User\Infrastructure\Security\UserIdentityProvider }
8 | msgphp_user_jwt: { id: MsgPhp\User\Infrastructure\Security\Jwt\UserIdentityProvider }
9 | firewalls:
10 | api_login:
11 | pattern: ^/api/login
12 | anonymous: true
13 | stateless: true
14 | provider: msgphp_user_jwt
15 | json_login:
16 | check_path: api_login
17 | username_path: email
18 | password_path: password
19 | success_handler: lexik_jwt_authentication.handler.authentication_success
20 | failure_handler: lexik_jwt_authentication.handler.authentication_failure
21 |
22 | api:
23 | pattern: ^/api
24 | anonymous: true
25 | stateless: true
26 | provider: msgphp_user_jwt
27 | guard:
28 | authenticators:
29 | - lexik_jwt_authentication.jwt_token_authenticator
30 |
31 | dev:
32 | pattern: ^/(_(profiler|wdt)|css|images|js)/
33 | security: false
34 | main:
35 | anonymous: lazy
36 | provider: msgphp_user
37 | user_checker: App\Security\UserChecker
38 | form_login:
39 | login_path: login
40 | check_path: login
41 | default_target_path: home
42 | username_parameter: email
43 | password_parameter: password
44 | remember_me: true
45 | remember_me:
46 | secret: '%kernel.secret%'
47 | remember_me_parameter: remember_me
48 | logout:
49 | path: logout
50 | switch_user:
51 | role: ROLE_ADMIN
52 | oauth:
53 | login_path: /login
54 | failure_path: /login
55 | resource_owners:
56 | google: /oauth/login-check/google
57 | facebook: /oauth/login-check/facebook
58 | oauth_user_provider:
59 | service: App\Security\OauthUserProvider
60 | guard:
61 | authenticators:
62 | - App\Security\OneTimeLoginAuthenticator
63 |
64 | # Easy way to control access for large sections of your site
65 | # Note: Only the *first* access control that matches will be used
66 | access_control:
67 | - { path: ^/profile, roles: ROLE_USER }
68 |
--------------------------------------------------------------------------------
/config/packages/sensio_framework_extra.yaml:
--------------------------------------------------------------------------------
1 | sensio_framework_extra:
2 | router:
3 | annotations: false
4 |
--------------------------------------------------------------------------------
/config/packages/test/framework.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | test: true
3 | session:
4 | storage_id: session.storage.mock_file
5 |
--------------------------------------------------------------------------------
/config/packages/test/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | handlers:
3 | main:
4 | type: fingers_crossed
5 | action_level: error
6 | handler: nested
7 | excluded_http_codes: [404, 405]
8 | channels: ["!event"]
9 | nested:
10 | type: stream
11 | path: "%kernel.logs_dir%/%kernel.environment%.log"
12 | level: debug
13 |
--------------------------------------------------------------------------------
/config/packages/test/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | strict_requirements: true
4 |
--------------------------------------------------------------------------------
/config/packages/test/twig.yaml:
--------------------------------------------------------------------------------
1 | twig:
2 | strict_variables: true
3 |
--------------------------------------------------------------------------------
/config/packages/test/validator.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | validation:
3 | not_compromised_password: false
4 |
--------------------------------------------------------------------------------
/config/packages/test/web_profiler.yaml:
--------------------------------------------------------------------------------
1 | web_profiler:
2 | toolbar: false
3 | intercept_redirects: false
4 |
5 | framework:
6 | profiler: { collect: false }
7 |
--------------------------------------------------------------------------------
/config/packages/translation.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | default_locale: en
3 | translator:
4 | default_path: '%kernel.project_dir%/translations'
5 | fallbacks:
6 | - en
7 |
--------------------------------------------------------------------------------
/config/packages/twig.yaml:
--------------------------------------------------------------------------------
1 | twig:
2 | default_path: '%kernel.project_dir%/templates'
3 |
--------------------------------------------------------------------------------
/config/packages/validator.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | validation:
3 | email_validation_mode: html5
4 |
5 | # Enables validator auto-mapping support.
6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata.
7 | #auto_mapping:
8 | # App\Entity\: []
9 |
--------------------------------------------------------------------------------
/config/routes.yaml:
--------------------------------------------------------------------------------
1 | # virtual routes
2 | logout:
3 | path: /logout
4 | methods: GET
5 |
6 | api_login:
7 | path: /api/login
8 | methods: POST
9 |
10 | oauth_login_check:
11 | path: /oauth/login-check/{resource}
12 |
--------------------------------------------------------------------------------
/config/routes/annotations.yaml:
--------------------------------------------------------------------------------
1 | controllers:
2 | resource: ../../src/Controller/
3 | type: annotation
4 |
5 | kernel:
6 | resource: ../../src/Kernel.php
7 | type: annotation
8 |
--------------------------------------------------------------------------------
/config/routes/api_platform.yaml:
--------------------------------------------------------------------------------
1 | api_platform:
2 | resource: .
3 | type: api_platform
4 | prefix: /api
5 |
--------------------------------------------------------------------------------
/config/routes/dev/framework.yaml:
--------------------------------------------------------------------------------
1 | _errors:
2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
3 | prefix: /_error
4 |
--------------------------------------------------------------------------------
/config/routes/dev/web_profiler.yaml:
--------------------------------------------------------------------------------
1 | web_profiler_wdt:
2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
3 | prefix: /_wdt
4 |
5 | web_profiler_profiler:
6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
7 | prefix: /_profiler
8 |
--------------------------------------------------------------------------------
/config/routes/hwi_oauth_routing.yaml:
--------------------------------------------------------------------------------
1 | hwi_oauth_redirect:
2 | resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
3 | prefix: /oauth/connect
4 |
5 | #hwi_oauth_connect:
6 | # resource: "@HWIOAuthBundle/Resources/config/routing/connect.xml"
7 | # prefix: /oauth/connect
8 |
9 | #hwi_oauth_login:
10 | # resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
11 | # prefix: /oauth/login
12 |
--------------------------------------------------------------------------------
/config/services.yaml:
--------------------------------------------------------------------------------
1 | # This file is the entry point to configure your own services.
2 | # Files in the packages/ subdirectory configure your dependencies.
3 |
4 | # Put parameters here that don't need to change on each machine where the app is deployed
5 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
6 | parameters:
7 |
8 | services:
9 | # default configuration for services in *this* file
10 | _defaults:
11 | autowire: true # Automatically injects dependencies in your services.
12 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
13 | bind:
14 | 'string $secret': '%kernel.secret%'
15 | 'callable $documentTransformer': '@MsgPhp\Domain\Infrastructure\Serializer\ProjectionDocumentTransformer'
16 | 'callable $documentTypeResolver': '@MsgPhp\Domain\Projection\ProjectionTypeResolver'
17 |
18 | # makes classes in src/ available to be used as services
19 | # this creates a service per class whose id is the fully-qualified class name
20 | App\:
21 | resource: '../src/*'
22 | exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
23 |
24 | # controllers are imported separately to make sure services can be injected
25 | # as action arguments even if you don't extend any base controller class
26 | App\Controller\:
27 | resource: '../src/Controller'
28 | tags: ['controller.service_arguments']
29 |
30 | # API
31 | App\Api\Endpoint\:
32 | resource: '../src/Api/Endpoint'
33 | tags: [controller.service_arguments]
34 |
35 | # Console
36 | App\Console\Command\:
37 | resource: '../src/Console/Command'
38 | MsgPhp\Domain\Infrastructure\Console\Context\ClassContextElementFactory: '@App\Console\ClassContextElementFactory'
39 |
40 | # Messenger
41 | App\EventSubscriber\:
42 | resource: '../src/EventSubscriber'
43 |
44 | # add more service definitions when explicit configuration is needed
45 | # please note that last definitions always *replace* previous ones
46 |
--------------------------------------------------------------------------------
/config/services_dev.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | _defaults:
3 | autowire: true
4 | autoconfigure: true
5 | public: false
6 |
7 | App\DataFixtures\:
8 | resource: '../src/DataFixtures'
9 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | tests
21 |
22 |
23 |
24 |
25 |
26 | src
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | handle($request);
28 | $response->send();
29 | $kernel->terminate($request, $response);
30 |
--------------------------------------------------------------------------------
/src/Api/DocumentIdentity.php:
--------------------------------------------------------------------------------
1 | toString();
21 | }
22 | if ('' === $value) {
23 | throw new \LogicException('A document identifier cannot be obtained from an empty identifier.');
24 | }
25 |
26 | return ShortUuid::uuid5(self::ID_NS, sha1($value));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Api/Endpoint/CreateUserEndpoint.php:
--------------------------------------------------------------------------------
1 | userId);
23 | $docId = DocumentIdentity::get($userId);
24 | $locationUrl = $urlGenerator->generate('api_users_get_item', ['id' => $docId], UrlGeneratorInterface::ABSOLUTE_URL);
25 |
26 | if (null === $data->password) {
27 | throw new BadRequestHttpException('Missing password field.');
28 | }
29 |
30 | $bus->dispatch(new CreateUser([
31 | 'id' => $userId,
32 | 'email' => $data->email,
33 | 'password' => $passwordHashing->encodePassword($data->password, null),
34 | ]));
35 |
36 | return new JsonResponse(['id' => $docId], JsonResponse::HTTP_CREATED, ['Location' => $locationUrl]);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Api/Endpoint/DeleteUserEndpoint.php:
--------------------------------------------------------------------------------
1 | dispatch(new DeleteUser(UserUuid::fromValue($data->userId)));
18 |
19 | return null;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Api/Projection/UserProjection.php:
--------------------------------------------------------------------------------
1 | getId();
23 |
24 | return [
25 | 'id' => DocumentIdentity::get($userId),
26 | 'user_id' => $userId->toString(),
27 | 'email' => $object->getEmail(),
28 | ];
29 | }
30 |
31 | public function supportsNormalization($data, $format = null): bool
32 | {
33 | return $data instanceof User;
34 | }
35 |
36 | public function hasCacheableSupportsMethod(): bool
37 | {
38 | return true;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Console/ClassContextElementFactory.php:
--------------------------------------------------------------------------------
1 | passwordHashing = $passwordHashing;
20 | }
21 |
22 | public function getElement(string $class, string $method, string $argument): ContextElement
23 | {
24 | $element = new ContextElement(ucfirst((string) preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1 \\2', '\\1 \\2'], $argument)));
25 |
26 | switch ($argument) {
27 | case 'email':
28 | $element->label = 'E-mail';
29 |
30 | break;
31 | case 'password':
32 | if (User::class === $class || EmailPassword::class === $class) {
33 | $element
34 | ->hide()
35 | ->generator(static function (): string {
36 | return bin2hex(random_bytes(8));
37 | })
38 | ->normalizer(function (string $value): string {
39 | return $this->passwordHashing->encodePassword($value, null);
40 | })
41 | ;
42 | }
43 |
44 | break;
45 | }
46 |
47 | return $element;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Console/Command/CreateOneTimeLoginTokenCommand.php:
--------------------------------------------------------------------------------
1 | em = $em;
29 | $this->userRepository = $userRepository;
30 | }
31 |
32 | protected function configure(): void
33 | {
34 | $this
35 | ->setDescription('Create a one-time login token')
36 | ->addArgument('username', InputArgument::REQUIRED, 'A username to provide the token for')
37 | ->addOption('token', null, InputOption::VALUE_REQUIRED, 'A pre-defined token (will be generated otherwise)')
38 | ;
39 | }
40 |
41 | protected function execute(InputInterface $input, OutputInterface $output): int
42 | {
43 | $io = new SymfonyStyle($input, $output);
44 | $token = $input->getOption('token');
45 |
46 | if (null !== $token) {
47 | if (!is_scalar($token)) {
48 | throw new \UnexpectedValueException('Unexpected token');
49 | }
50 |
51 | $token = (string) $token;
52 |
53 | if (null !== $this->em->find(OneTimeLoginToken::class, $token)) {
54 | throw new \LogicException(sprintf('The token "%s" already exists.', $token));
55 | }
56 | }
57 |
58 | $username = $input->getArgument('username');
59 | $user = $this->userRepository->findByUsername($username);
60 | $oneTimeLoginToken = new OneTimeLoginToken($user, $token);
61 |
62 | $this->em->persist($oneTimeLoginToken);
63 | $this->em->flush();
64 |
65 | /** @psalm-suppress UndefinedInterfaceMethod */
66 | $io->success(sprintf('Created login token "%s" for user "%s".', $oneTimeLoginToken->getToken(), $oneTimeLoginToken->getUser()->getCredential()->getUsername()));
67 |
68 | return 0;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Console/Command/InviteUserCommand.php:
--------------------------------------------------------------------------------
1 | em = $em;
32 | $this->userRepository = $userRepository;
33 | $this->mailer = $mailer;
34 | }
35 |
36 | protected function configure(): void
37 | {
38 | $this
39 | ->setDescription('Invite a user for registration')
40 | ->addArgument('email', InputArgument::REQUIRED, 'An e-mail to provide the token for')
41 | ->addOption('token', null, InputOption::VALUE_REQUIRED, 'A pre-defined token (will be generated otherwise)')
42 | ->addOption('notify', null, InputOption::VALUE_NONE, 'Send notification to the invited e-mail')
43 | ;
44 | }
45 |
46 | protected function execute(InputInterface $input, OutputInterface $output): int
47 | {
48 | $io = new SymfonyStyle($input, $output);
49 | $token = $input->getOption('token');
50 |
51 | if (null !== $token) {
52 | if (!is_scalar($token)) {
53 | throw new \UnexpectedValueException('Unexpected token');
54 | }
55 |
56 | $token = (string) $token;
57 |
58 | if (null !== $this->em->find(UserInvitation::class, $token)) {
59 | throw new \LogicException(sprintf('The token "%s" already exists.', $token));
60 | }
61 | }
62 |
63 | /** @psalm-suppress PossiblyInvalidCast */
64 | $email = (string) $input->getArgument('email');
65 |
66 | if ($this->userRepository->usernameExists($email) || null !== $this->em->getRepository(UserInvitation::class)->findOneBy(['email' => $email])) {
67 | throw new \LogicException(sprintf('The username "%s" already exists.', $email));
68 | }
69 |
70 | $invitation = new UserInvitation($email, $token);
71 |
72 | $this->em->persist($invitation);
73 | $this->em->flush();
74 |
75 | $io->success(sprintf('Created registration token "%s" for user "%s".', $invitation->getToken(), $invitation->getEmail()));
76 |
77 | if ($input->getOption('notify')) {
78 | $params = compact('invitation');
79 | $message = (new TemplatedEmail())
80 | ->from('webmaster@localhost')
81 | ->to($email)
82 | ->subject('You are invited to register at The App')
83 | ->textTemplate('user/email/invited.txt.twig')
84 | ->htmlTemplate('user/email/invited.html.twig')
85 | ->context($params)
86 | ;
87 |
88 | $this->mailer->send($message);
89 | $io->note('Notification sent');
90 | } else {
91 | $io->note('No notification was sent');
92 | }
93 |
94 | return 0;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Controller/ConfirmEmailController.php:
--------------------------------------------------------------------------------
1 | dispatch(new ConfirmUserEmail($userEmail->getEmail()));
33 |
34 | return $responder->respond((new RespondRouteRedirect('profile'))->withFlashes([
35 | 'success' => sprintf('Hi %s, your e-mail is confirmed.', $user->getEmail()),
36 | ]));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Controller/ConfirmRegistrationController.php:
--------------------------------------------------------------------------------
1 | dispatch(new ConfirmUser($user->getId()));
30 |
31 | return $responder->respond((new RespondRouteRedirect('login'))->withFlashes([
32 | 'success' => sprintf('Hi %s, your registration is confirmed. You can now login.', $user->getEmail()),
33 | ]));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Controller/ForgotPasswordController.php:
--------------------------------------------------------------------------------
1 | createNamed('', ForgotPasswordType::class);
31 | $form->handleRequest($request);
32 |
33 | if ($form->isSubmitted() && $form->isValid()) {
34 | $data = $form->getData();
35 |
36 | if (isset($data['user'])) {
37 | /** @var User $user */
38 | $user = $data['user'];
39 | $bus->dispatch(new RequestUserPassword($user->getId()));
40 | }
41 |
42 | return $responder->respond((new RespondRouteRedirect('home'))->withFlashes([
43 | 'success' => sprintf('Hi %s, we\'ve send you a password reset link.', $data['email']),
44 | ]));
45 | }
46 |
47 | return $responder->respond(new RespondTemplate('user/forgot_password.html.twig', [
48 | 'form' => $form->createView(),
49 | ]));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Controller/HomeController.php:
--------------------------------------------------------------------------------
1 | respond(new RespondTemplate('home.html.twig'));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Controller/LoginController.php:
--------------------------------------------------------------------------------
1 | createNamed('', LoginType::class, [
30 | 'email' => $authenticationUtils->getLastUsername(),
31 | ]);
32 |
33 | // one-time login
34 | $oneTimeLoginForm = $formFactory->createNamed('', OneTimeLoginType::class);
35 |
36 | return $responder->respond(new RespondTemplate('user/login.html.twig', [
37 | 'error' => $authenticationUtils->getLastAuthenticationError(),
38 | 'form' => $form->createView(),
39 | 'oneTimeLoginForm' => $oneTimeLoginForm->createView(),
40 | ]));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Controller/ProfileController.php:
--------------------------------------------------------------------------------
1 | query->getBoolean('generate-jwt')) {
51 | return $responder->respond((new RespondRouteRedirect('profile'))->withFlashes([
52 | 'success' => sprintf('Generated JWT token: %s', $jwtTokenManager->create($securityUser)),
53 | ]));
54 | }
55 |
56 | // add email
57 | $emailForm = $formFactory->create(AddEmailType::class);
58 | $emailForm->handleRequest($request);
59 |
60 | if ($emailForm->isSubmitted() && $emailForm->isValid()) {
61 | $bus->dispatch(new AddUserEmail($user->getId(), $email = $emailForm->getData()['email']));
62 |
63 | return $responder->respond((new RespondRouteRedirect('profile'))->withFlashes([
64 | 'success' => sprintf('E-mail %s added. We\'ve send you a confirmation link.', $email),
65 | ]));
66 | }
67 |
68 | // mark primary email
69 | if ($primaryEmail = $request->query->get('primary-email')) {
70 | try {
71 | /** @var UserEmail $userEmail */
72 | $userEmail = $user->getEmails()->get($primaryEmail);
73 | if (!$userEmail->isConfirmed()) {
74 | $userEmail = null;
75 | }
76 | } catch (UnknownCollectionElement $e) {
77 | $userEmail = null;
78 | }
79 | if (null === $userEmail) {
80 | return $responder->respond(new RespondNotFound());
81 | }
82 |
83 | $confirmResponse = $passwordConfirmation->confirm($request);
84 |
85 | if (null !== $confirmResponse) {
86 | return $confirmResponse;
87 | }
88 |
89 | $currentEmail = $user->getEmail();
90 | $bus->dispatch(new DeleteUserEmail($primaryEmail));
91 | $bus->dispatch(new ChangeUserCredential($user->getId(), ['email' => $primaryEmail]));
92 | $bus->dispatch(new AddUserEmail($user->getId(), $currentEmail, ['confirm' => true]));
93 |
94 | return $responder->respond((new RespondRouteRedirect('profile'))->withFlashes([
95 | 'success' => sprintf('E-mail %s marked primary.', $primaryEmail),
96 | ]));
97 | }
98 |
99 | // send email confirmation link
100 | if ($confirmEmail = $request->query->get('confirm-email')) {
101 | try {
102 | /** @var UserEmail $userEmail */
103 | $userEmail = $user->getEmails()->get($confirmEmail);
104 | if ($userEmail->isConfirmed()) {
105 | $userEmail = null;
106 | }
107 | } catch (UnknownCollectionElement $e) {
108 | $userEmail = null;
109 | }
110 | if (null === $userEmail) {
111 | return $responder->respond(new RespondNotFound());
112 | }
113 |
114 | $sendEmailConfirmationUrl->notify($userEmail);
115 |
116 | return $responder->respond((new RespondRouteRedirect('profile'))->withFlashes([
117 | 'success' => 'We\'ve send you a e-mail confirmation link.',
118 | ]));
119 | }
120 |
121 | // delete email
122 | if ($deleteEmail = $request->query->get('delete-email')) {
123 | if (!$user->getEmails()->containsKey($deleteEmail)) {
124 | return $responder->respond(new RespondNotFound());
125 | }
126 |
127 | $confirmResponse = $passwordConfirmation->confirm($request);
128 |
129 | if (null !== $confirmResponse) {
130 | return $confirmResponse;
131 | }
132 |
133 | $bus->dispatch(new DeleteUserEmail($deleteEmail));
134 |
135 | return $responder->respond((new RespondRouteRedirect('profile'))->withFlashes([
136 | 'success' => sprintf('E-mail %s deleted.', $deleteEmail),
137 | ]));
138 | }
139 |
140 | // change password
141 | $passwordForm = $formFactory->create(ChangePasswordType::class);
142 | $passwordForm->handleRequest($request);
143 |
144 | if ($passwordForm->isSubmitted() && $passwordForm->isValid()) {
145 | $bus->dispatch(new ChangeUserCredential($user->getId(), $passwordForm->getData()));
146 |
147 | return $responder->respond((new RespondRouteRedirect('profile'))->withFlashes([
148 | 'success' => 'Your password is now changed.',
149 | ]));
150 | }
151 |
152 | // render view
153 | return $responder->respond(new RespondTemplate('user/profile.html.twig', [
154 | 'email_form' => $emailForm->createView(),
155 | 'password_form' => $passwordForm->createView(),
156 | ]));
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/Controller/RegisterController.php:
--------------------------------------------------------------------------------
1 | query->get('token')) && null !== $invitation = $em->find(UserInvitation::class, $token)) {
34 | $data['email'] = $invitation->getEmail();
35 | }
36 |
37 | $form = $formFactory->createNamed('', RegisterType::class, $data);
38 | $form->handleRequest($request);
39 |
40 | if ($form->isSubmitted() && $form->isValid()) {
41 | $data = $form->getData();
42 | $data['invitation_token'] = $token;
43 | $bus->dispatch(new CreateUser($data));
44 |
45 | return $responder->respond((new RespondRouteRedirect('home'))->withFlashes([
46 | 'success' => sprintf('Hi %s, you\'re successfully registered. We\'ve send you a confirmation link.', $data['email']),
47 | ]));
48 | }
49 |
50 | return $responder->respond(new RespondTemplate('user/register.html.twig', [
51 | 'form' => $form->createView(),
52 | ]));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Controller/ResetPasswordController.php:
--------------------------------------------------------------------------------
1 | createNamed('', ResetPasswordType::class);
36 | $form->handleRequest($request);
37 |
38 | if ($form->isSubmitted() && $form->isValid()) {
39 | $bus->dispatch(new ResetUserPassword($user->getId(), $form->getData()['password']));
40 |
41 | return $responder->respond((new RespondRouteRedirect('home'))->withFlashes([
42 | 'success' => sprintf('Hi %s, we\'ve reset your password.', $user->getEmail()),
43 | ]));
44 | }
45 |
46 | return $responder->respond(new RespondTemplate('user/reset_password.html.twig', [
47 | 'form' => $form->createView(),
48 | ]));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/DataFixtures/AppFixtures.php:
--------------------------------------------------------------------------------
1 | passwordEncoder = $passwordEncoder;
32 | }
33 |
34 | public function load(ObjectManager $manager): void
35 | {
36 | // roles
37 | $manager->persist($adminRole = new Role(RoleProvider::ROLE_ADMIN));
38 |
39 | // attributes
40 | $manager->persist($this->createAttribute(Attribute::GOOGLE_OAUTH_ID));
41 | $manager->persist($this->createAttribute(Attribute::FACEBOOK_OAUTH_ID));
42 | $manager->persist($boolAttr = $this->createAttribute());
43 | $manager->persist($intAttr = $this->createAttribute());
44 | $manager->persist($floatAttr = $this->createAttribute());
45 | $manager->persist($stringAttr = $this->createAttribute());
46 | $manager->persist($dateTimeAttr = $this->createAttribute());
47 |
48 | // users
49 | $user = $this->createUser('user@domain.dev');
50 | $user->enable();
51 | $user->confirm();
52 | $manager->persist($user);
53 | $manager->persist(new UserEmail($user, 'other@domain.dev'));
54 | $manager->persist(new UserEmail($user, 'secondary@domain.dev', true));
55 | $manager->persist($this->createUserAttributeValue($user, $boolAttr, true));
56 | $manager->persist($this->createUserAttributeValue($user, $boolAttr, false));
57 | $manager->persist($this->createUserAttributeValue($user, $boolAttr, null));
58 | $manager->persist($this->createUserAttributeValue($user, $intAttr, 123));
59 | $manager->persist($this->createUserAttributeValue($user, $intAttr, -456));
60 | $manager->persist($this->createUserAttributeValue($user, $floatAttr, 123.0123456789));
61 | $manager->persist($this->createUserAttributeValue($user, $floatAttr, -0.123));
62 | $manager->persist($this->createUserAttributeValue($user, $stringAttr, 'text'));
63 | $manager->persist($this->createUserAttributeValue($user, $dateTimeAttr, new \DateTimeImmutable()));
64 |
65 | $user = $this->createUser('user+disabled@domain.dev');
66 | $manager->persist($user);
67 |
68 | $user = $this->createUser('user+admin@domain.dev');
69 | $user->enable();
70 | $user->confirm();
71 | $manager->persist($user);
72 | $manager->persist(new UserRole($user, $adminRole));
73 |
74 | $user = $this->createUser('user+admin+disabled@domain.dev');
75 | $manager->persist($user);
76 | $manager->persist(new UserRole($user, $adminRole));
77 |
78 | $premiumUser = $this->createUser('user+premium@domain.dev', true);
79 | $premiumUser->enable();
80 | $premiumUser->confirm();
81 | $manager->persist($premiumUser);
82 |
83 | $manager->flush();
84 | }
85 |
86 | private function createAttribute($id = null): Attribute
87 | {
88 | return new Attribute(AttributeUuid::fromValue($id));
89 | }
90 |
91 | private function createUser(string $email, bool $premium = false, string $password = self::PASSWORD): User
92 | {
93 | $password = $this->passwordEncoder->encodePassword($password, null);
94 |
95 | if ($premium) {
96 | return new PremiumUser(new UserUuid(), $email, $password);
97 | }
98 |
99 | return new User(new UserUuid(), $email, $password);
100 | }
101 |
102 | private function createUserAttributeValue(User $user, Attribute $attribute, $value): UserAttributeValue
103 | {
104 | return new UserAttributeValue($user, new AttributeValue(new AttributeValueUuid(), $attribute, $value));
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Entity/Attribute.php:
--------------------------------------------------------------------------------
1 | id = $id;
27 | }
28 |
29 | public function getId(): AttributeId
30 | {
31 | return $this->id;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Entity/AttributeValue.php:
--------------------------------------------------------------------------------
1 | id = $id;
26 | }
27 |
28 | public function getId(): AttributeValueId
29 | {
30 | return $this->id;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Entity/OneTimeLoginToken.php:
--------------------------------------------------------------------------------
1 | user = $user;
28 | $this->token = $token ?? bin2hex(random_bytes(32));
29 | $this->redirectUrl = $redirectUrl;
30 | }
31 |
32 | public function getToken(): string
33 | {
34 | return $this->token;
35 | }
36 |
37 | public function getRedirectUrl(): ?string
38 | {
39 | return $this->redirectUrl;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Entity/PremiumUser.php:
--------------------------------------------------------------------------------
1 | name = $name;
23 | }
24 |
25 | public function getName(): string
26 | {
27 | return $this->name;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Entity/User.php:
--------------------------------------------------------------------------------
1 | id = $id;
46 | $this->createdAt = new \DateTimeImmutable();
47 | $this->credential = new EmailPassword($email, $password);
48 | $this->confirmationToken = bin2hex(random_bytes(32));
49 | }
50 |
51 | public function getId(): UserId
52 | {
53 | return $this->id;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Entity/UserAttributeValue.php:
--------------------------------------------------------------------------------
1 | confirm();
33 | } else {
34 | $this->confirmationToken = bin2hex(random_bytes(32));
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Entity/UserInvitation.php:
--------------------------------------------------------------------------------
1 | token = $token ?? bin2hex(random_bytes(32));
25 | $this->email = $email;
26 | }
27 |
28 | public function getToken(): string
29 | {
30 | return $this->token;
31 | }
32 |
33 | public function getEmail(): string
34 | {
35 | return $this->email;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Entity/UserRole.php:
--------------------------------------------------------------------------------
1 | bus = $bus;
20 | }
21 |
22 | public function __invoke(UserConfirmed $event): void
23 | {
24 | /** @psalm-suppress ArgumentTypeCoercion */
25 | $this->notify($event->user);
26 | }
27 |
28 | public function notify(User $user): void
29 | {
30 | if ($user->isEnabled()) {
31 | return;
32 | }
33 |
34 | $this->bus->dispatch(new EnableUser($user->getId()));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/EventSubscriber/InvalidateUserInvitation.php:
--------------------------------------------------------------------------------
1 | em = $em;
19 | }
20 |
21 | public function __invoke(UserCreated $event): void
22 | {
23 | if (!isset($event->context['invitation_token']) || null === $invitation = $this->em->find(UserInvitation::class, $event->context['invitation_token'])) {
24 | return;
25 | }
26 |
27 | $this->em->remove($invitation);
28 | $this->em->flush();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/EventSubscriber/SendEmailConfirmationUrl.php:
--------------------------------------------------------------------------------
1 | mailer = $mailer;
20 | }
21 |
22 | public function __invoke(UserEmailAdded $event): void
23 | {
24 | /** @psalm-suppress ArgumentTypeCoercion */
25 | $this->notify($event->userEmail);
26 | }
27 |
28 | public function notify(UserEmail $userEmail): void
29 | {
30 | if ($userEmail->isConfirmed()) {
31 | return;
32 | }
33 |
34 | $params = ['userEmail' => $userEmail];
35 | $message = (new TemplatedEmail())
36 | ->from('webmaster@localhost')
37 | ->to($userEmail->getEmail())
38 | ->subject('Confirm your e-mail at The App')
39 | ->textTemplate('user/email/confirm_email.txt.twig')
40 | ->htmlTemplate('user/email/confirm_email.html.twig')
41 | ->context($params)
42 | ;
43 |
44 | $this->mailer->send($message);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/EventSubscriber/SendPasswordResetUrl.php:
--------------------------------------------------------------------------------
1 | mailer = $mailer;
20 | }
21 |
22 | public function __invoke(UserPasswordRequested $event): void
23 | {
24 | /** @psalm-suppress ArgumentTypeCoercion */
25 | $this->notify($event->user);
26 | }
27 |
28 | public function notify(User $user): void
29 | {
30 | if (null === $user->getPasswordResetToken()) {
31 | return;
32 | }
33 |
34 | $params = ['user' => $user];
35 | $message = (new TemplatedEmail())
36 | ->from('webmaster@localhost')
37 | ->to($user->getEmail())
38 | ->subject('Reset your password at The App')
39 | ->textTemplate('user/email/reset_password.txt.twig')
40 | ->htmlTemplate('user/email/reset_password.html.twig')
41 | ->context($params)
42 | ;
43 |
44 | $this->mailer->send($message);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/EventSubscriber/SendRegistrationConfirmationUrl.php:
--------------------------------------------------------------------------------
1 | mailer = $mailer;
20 | }
21 |
22 | public function __invoke(UserCreated $event): void
23 | {
24 | /** @psalm-suppress ArgumentTypeCoercion */
25 | $this->notify($event->user);
26 | }
27 |
28 | public function notify(User $user): void
29 | {
30 | if ($user->isConfirmed()) {
31 | return;
32 | }
33 |
34 | $params = ['user' => $user];
35 | $message = (new TemplatedEmail())
36 | ->from('webmaster@localhost')
37 | ->to($user->getEmail())
38 | ->subject('Confirm your account at The App')
39 | ->textTemplate('user/email/confirm_registration.txt.twig')
40 | ->htmlTemplate('user/email/confirm_registration.html.twig')
41 | ->context($params)
42 | ;
43 |
44 | $this->mailer->send($message);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/EventSubscriber/SynchronizeApi.php:
--------------------------------------------------------------------------------
1 | bus = $bus;
26 | $this->documentTransformer = $documentTransformer;
27 | $this->documentTypeResolver = $documentTypeResolver;
28 | }
29 |
30 | public function __invoke($event): void
31 | {
32 | if ($event instanceof UserCreated || $event instanceof UserCredentialChanged) {
33 | $this->notifySave($event->user);
34 |
35 | return;
36 | }
37 |
38 | if ($event instanceof UserDeleted) {
39 | $this->notifyDelete($event->user, $event->user->getId());
40 |
41 | return;
42 | }
43 | }
44 |
45 | public static function getHandledMessages(): iterable
46 | {
47 | return [
48 | UserCreated::class,
49 | UserCredentialChanged::class,
50 | UserDeleted::class,
51 | ];
52 | }
53 |
54 | private function notifySave(object $object): void
55 | {
56 | $this->bus->dispatch(new SaveProjection(($this->documentTypeResolver)($object), ($this->documentTransformer)($object)));
57 | }
58 |
59 | private function notifyDelete(object $object, DomainId $id): void
60 | {
61 | $this->bus->dispatch(new DeleteProjection(($this->documentTypeResolver)($object), DocumentIdentity::get($id)));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Form/AddEmailType.php:
--------------------------------------------------------------------------------
1 | add('email', EmailType::class, [
19 | 'constraints' => [new NotBlank(), new Email(), new UniqueUsername()],
20 | ]);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Form/ChangePasswordType.php:
--------------------------------------------------------------------------------
1 | add('current', HashedPasswordType::class, [
18 | 'password_options' => ['constraints' => new UserPassword()],
19 | 'mapped' => false,
20 | ]);
21 | $builder->add('password', HashedPasswordType::class, [
22 | 'password_confirm' => true,
23 | 'password_options' => ['constraints' => new NotBlank()],
24 | ]);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Form/ForgotPasswordType.php:
--------------------------------------------------------------------------------
1 | add('email', EmailType::class, [
18 | 'constraints' => [new NotBlank()],
19 | ]);
20 | }
21 |
22 | public function configureOptions(OptionsResolver $resolver): void
23 | {
24 | $resolver->setDefault('user_mapping', ['email' => 'user']);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Form/LoginType.php:
--------------------------------------------------------------------------------
1 | add('email', EmailType::class);
18 | $builder->add('password', PasswordType::class);
19 | $builder->add('remember_me', CheckboxType::class);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Form/OneTimeLoginType.php:
--------------------------------------------------------------------------------
1 | add('token', TextType::class, [
19 | 'constraints' => new NotBlank(),
20 | ]);
21 | }
22 |
23 | public function configureOptions(OptionsResolver $resolver): void
24 | {
25 | $resolver->setDefaults([
26 | 'method' => Request::METHOD_GET,
27 | 'csrf_protection' => false,
28 | ]);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Form/RegisterType.php:
--------------------------------------------------------------------------------
1 | add('email', EmailType::class, [
20 | 'constraints' => [new NotBlank(), new Email(), new UniqueUsername()],
21 | ]);
22 | $builder->add('password', HashedPasswordType::class, [
23 | 'password_confirm' => true,
24 | 'password_options' => ['constraints' => new NotBlank()],
25 | ]);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Form/ResetPasswordType.php:
--------------------------------------------------------------------------------
1 | add('password', HashedPasswordType::class, [
17 | 'password_confirm' => true,
18 | 'password_options' => ['constraints' => new NotBlank()],
19 | ]);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Http/Respond.php:
--------------------------------------------------------------------------------
1 | status = $status;
17 |
18 | return $respond;
19 | }
20 |
21 | public function withHeaders(array $headers): self
22 | {
23 | $respond = clone $this;
24 | $respond->headers = $headers;
25 |
26 | return $respond;
27 | }
28 |
29 | public function withFlashes(array $flashes): self
30 | {
31 | $respond = clone $this;
32 | $respond->flashes = $flashes;
33 |
34 | return $respond;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Http/RespondBadRequest.php:
--------------------------------------------------------------------------------
1 | status = Response::HTTP_BAD_REQUEST;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Http/RespondEmpty.php:
--------------------------------------------------------------------------------
1 | status = Response::HTTP_NO_CONTENT;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Http/RespondFile.php:
--------------------------------------------------------------------------------
1 | file = $file;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Http/RespondJson.php:
--------------------------------------------------------------------------------
1 | data = $data;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Http/RespondNotFound.php:
--------------------------------------------------------------------------------
1 | status = Response::HTTP_NOT_FOUND;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Http/RespondRaw.php:
--------------------------------------------------------------------------------
1 | contents = $contents;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Http/RespondRawJson.php:
--------------------------------------------------------------------------------
1 | status = Response::HTTP_FOUND;
16 | $this->url = $url;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Http/RespondRouteRedirect.php:
--------------------------------------------------------------------------------
1 | status = Response::HTTP_FOUND;
19 | $this->name = $name;
20 | $this->parameters = $parameters;
21 | $this->referenceType = $referenceType;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Http/RespondStream.php:
--------------------------------------------------------------------------------
1 | callback = $callback;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Http/RespondTemplate.php:
--------------------------------------------------------------------------------
1 | name = $name;
15 | $this->context = $context;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Http/Responder.php:
--------------------------------------------------------------------------------
1 | urlGenerator = $urlGenerator;
27 | $this->twig = $twig;
28 | $this->flashBag = $flashBag;
29 | }
30 |
31 | public function respond(Respond $respond): Response
32 | {
33 | foreach ($respond->flashes as $type => $messages) {
34 | foreach ((array) $messages as $message) {
35 | $this->flashBag->add($type, $message);
36 | }
37 | }
38 |
39 | if ($respond instanceof RespondBadRequest) {
40 | throw new BadRequestHttpException(null, null, 0, $respond->headers);
41 | }
42 |
43 | if ($respond instanceof RespondNotFound) {
44 | throw new NotFoundHttpException(null, null, 0, $respond->headers);
45 | }
46 |
47 | if ($respond instanceof RespondTemplate) {
48 | return new Response($this->twig->render($respond->name, $respond->context), $respond->status, $respond->headers);
49 | }
50 |
51 | if ($respond instanceof RespondRouteRedirect) {
52 | return new RedirectResponse($this->urlGenerator->generate($respond->name, $respond->parameters, $respond->referenceType), $respond->status, $respond->headers);
53 | }
54 |
55 | if ($respond instanceof RespondRedirect) {
56 | return new RedirectResponse($respond->url, $respond->status, $respond->headers);
57 | }
58 |
59 | if ($respond instanceof RespondJson) {
60 | return new JsonResponse($respond->data, $respond->status, $respond->headers);
61 | }
62 |
63 | if ($respond instanceof RespondRawJson) {
64 | return JsonResponse::fromJsonString($respond->contents, $respond->status, $respond->headers);
65 | }
66 |
67 | if ($respond instanceof RespondFile) {
68 | return new BinaryFileResponse($respond->file, $respond->status, $respond->headers);
69 | }
70 |
71 | if ($respond instanceof RespondStream) {
72 | return new StreamedResponse($respond->callback, $respond->status, $respond->headers);
73 | }
74 |
75 | if ($respond instanceof RespondRaw) {
76 | return new Response($respond->contents, $respond->status, $respond->headers);
77 | }
78 |
79 | return new Response('', $respond->status, $respond->headers);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Kernel.php:
--------------------------------------------------------------------------------
1 | getProjectDir().'/config/bundles.php';
23 | foreach ($contents as $class => $envs) {
24 | if ($envs[$this->environment] ?? $envs['all'] ?? false) {
25 | yield new $class();
26 | }
27 | }
28 | }
29 |
30 | public function getProjectDir(): string
31 | {
32 | return \dirname(__DIR__);
33 | }
34 |
35 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
36 | {
37 | $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));
38 | $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug);
39 | $container->setParameter('container.dumper.inline_factories', true);
40 | $confDir = $this->getProjectDir().'/config';
41 |
42 | $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');
43 | $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');
44 | $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
45 | $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
46 | }
47 |
48 | protected function configureRoutes(RouteCollectionBuilder $routes): void
49 | {
50 | $confDir = $this->getProjectDir().'/config';
51 |
52 | $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob');
53 | $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');
54 | $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Security/JwtTokenSubscriber.php:
--------------------------------------------------------------------------------
1 | urlGenerator = $urlGenerator;
22 | }
23 |
24 | public function handleSuccess(AuthenticationSuccessEvent $event): void
25 | {
26 | $docId = DocumentIdentity::get($event->getUser()->getUsername());
27 | $locationUrl = $this->urlGenerator->generate('api_users_get_item', ['id' => $docId], UrlGeneratorInterface::ABSOLUTE_URL);
28 |
29 | $event->getResponse()->headers->set('Location', $locationUrl);
30 | }
31 |
32 | public function handleFailure(JWTFailureEventInterface $event): void
33 | {
34 | $exception = $event->getException();
35 |
36 | throw new UnauthorizedHttpException('Bearer', $exception->getMessage(), $exception);
37 | }
38 |
39 | public static function getSubscribedEvents(): array
40 | {
41 | return [
42 | Events::AUTHENTICATION_SUCCESS => 'handleSuccess',
43 | Events::JWT_EXPIRED => 'handleFailure',
44 | Events::JWT_INVALID => 'handleFailure',
45 | Events::JWT_NOT_FOUND => 'handleFailure',
46 | ];
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Security/OauthUserProvider.php:
--------------------------------------------------------------------------------
1 | userRepository = $userRepository;
34 | $this->userAttributeValueRepository = $userAttributeValueRepository;
35 | $this->userIdentityProvider = $userIdentityProvider;
36 | $this->bus = $bus;
37 | }
38 |
39 | public function loadUserByOAuthUserResponse(UserResponseInterface $response): UserInterface
40 | {
41 | $owner = $response->getResourceOwner()->getName();
42 | $username = $response->getUsername();
43 |
44 | if (!\defined($const = Attribute::class.'::'.strtoupper($owner).'_OAUTH_ID')) {
45 | throw new \LogicException(sprintf('Missing constant "%s" for OAuth resoure owner "%s"', $const, $owner));
46 | }
47 |
48 | $attributeId = AttributeUuid::fromValue(\constant($const));
49 | $userAttributeValues = $this->userAttributeValueRepository->findAllByAttributeIdAndValue($attributeId, $username);
50 |
51 | if ($userAttributeValues->isEmpty()) {
52 | if (null === $email = $response->getEmail()) {
53 | throw new CustomUserMessageAuthenticationException(sprintf('Oauth resource owner "%s" requires e-mail availability and appropriate read-privilege.', $owner));
54 | }
55 |
56 | try {
57 | $user = $this->userRepository->findByUsername($email);
58 | $userId = $user->getId();
59 | } catch (EntityNotFound $e) {
60 | $userId = new UserUuid();
61 | // @todo validate username/email availability
62 | $this->bus->dispatch(new CreateUser([
63 | 'id' => $userId,
64 | 'email' => $email,
65 | 'password' => bin2hex(random_bytes(32)),
66 | ]));
67 | $this->bus->dispatch(new ConfirmUser($userId));
68 |
69 | $user = $this->userRepository->find($userId);
70 | }
71 |
72 | $this->bus->dispatch(new AddUserAttributeValue($userId, $attributeId, $username));
73 | } else {
74 | /** @var UserAttributeValue $userAttributeValue */
75 | $userAttributeValue = $userAttributeValues->first();
76 | $user = $userAttributeValue->getUser();
77 | }
78 |
79 | return $this->userIdentityProvider->fromUser($user);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Security/OneTimeLoginAuthenticator.php:
--------------------------------------------------------------------------------
1 | em = $em;
30 | $this->userIdentityProvider = $userIdentityProvider;
31 | $this->urlGenerator = $urlGenerator;
32 | }
33 |
34 | public function start(Request $request, ?AuthenticationException $authException = null): Response
35 | {
36 | return new RedirectResponse($this->urlGenerator->generate('login'));
37 | }
38 |
39 | public function supports(Request $request): bool
40 | {
41 | return 'login' === $request->attributes->get('_route')
42 | && $request->isMethod(Request::METHOD_GET)
43 | && $request->query->has('token');
44 | }
45 |
46 | public function getCredentials(Request $request)
47 | {
48 | return $request->query->get('token');
49 | }
50 |
51 | public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
52 | {
53 | if (null === $oneTimeLoginToken = $this->getOneTimeLoginToken($credentials)) {
54 | return null;
55 | }
56 |
57 | return $this->userIdentityProvider->fromUser($oneTimeLoginToken->getUser());
58 | }
59 |
60 | public function checkCredentials($credentials, UserInterface $identity): bool
61 | {
62 | if (!$identity instanceof UserIdentity) {
63 | return false;
64 | }
65 |
66 | if (null === $oneTimeLoginToken = $this->getOneTimeLoginToken($credentials)) {
67 | return false;
68 | }
69 |
70 | return $oneTimeLoginToken->getUserId()->equals($identity->getUserId());
71 | }
72 |
73 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
74 | {
75 | return new RedirectResponse($this->urlGenerator->generate('login'));
76 | }
77 |
78 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
79 | {
80 | if (null === $oneTimeLoginToken = $this->getOneTimeLoginTokenOnce($this->getCredentials($request))) {
81 | return null;
82 | }
83 |
84 | return new RedirectResponse($oneTimeLoginToken->getRedirectUrl() ?? $this->urlGenerator->generate('profile'));
85 | }
86 |
87 | public function supportsRememberMe(): bool
88 | {
89 | return false;
90 | }
91 |
92 | private function getOneTimeLoginToken(string $token): ?OneTimeLoginToken
93 | {
94 | /** @var null|OneTimeLoginToken */
95 | return $this->em->find(OneTimeLoginToken::class, $token);
96 | }
97 |
98 | private function getOneTimeLoginTokenOnce(string $token): ?OneTimeLoginToken
99 | {
100 | if (null !== $token = $this->getOneTimeLoginToken($token)) {
101 | $this->em->remove($token);
102 | $this->em->flush();
103 | }
104 |
105 | return $token;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Security/PasswordConfirmation.php:
--------------------------------------------------------------------------------
1 | secret = $secret;
28 | $this->twig = $twig;
29 | $this->formFactory = $formFactory;
30 | $this->urlGenerator = $urlGenerator;
31 | }
32 |
33 | public function confirm(Request $request): ?Response
34 | {
35 | $session = $request->getSession();
36 |
37 | /** @psalm-suppress DocblockTypeContradiction */
38 | if (null === $session) {
39 | throw new \LogicException('Session not available.');
40 | }
41 |
42 | $hash = md5(implode("\0", [
43 | $this->secret,
44 | $request->getClientIp(),
45 | $request->getUriForPath($request->getRequestUri()),
46 | ]));
47 |
48 | if ($session->has($hash)) {
49 | if ($session->remove($hash) !== md5($hash.$this->secret)) {
50 | throw new BadRequestHttpException('Unable to confirm current request.');
51 | }
52 |
53 | return null;
54 | }
55 |
56 | $referer = (null !== $route = $request->attributes->get('_route'))
57 | ? $this->urlGenerator->generate($route, $request->attributes->get('_route_params', []), UrlGeneratorInterface::ABSOLUTE_URL)
58 | : $request->headers->get('referer');
59 |
60 | $form = $this->formFactory->createNamedBuilder($hash)
61 | ->add('password', HashedPasswordType::class, [
62 | 'password_options' => ['constraints' => new UserPassword()],
63 | ])
64 | ->add('referer', HiddenType::class, [
65 | 'data' => $referer,
66 | ])
67 | ->getForm()
68 | ;
69 |
70 | $form->handleRequest($request);
71 |
72 | if ($form->isSubmitted()) {
73 | if ($form->isValid()) {
74 | $session->set($hash, md5($hash.$this->secret));
75 |
76 | return new RedirectResponse($request->getRequestUri());
77 | }
78 |
79 | $referer = $form->get('referer')->getData();
80 | }
81 |
82 | if (null === $referer) {
83 | throw new BadRequestHttpException('Unable to confirm current request.');
84 | }
85 |
86 | return new Response($this->twig->render('password_confirmation.html.twig', [
87 | 'form' => $form->createView(),
88 | 'cancelUrl' => $referer,
89 | ]));
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Security/RoleProvider.php:
--------------------------------------------------------------------------------
1 | isEnabled() ? [self::ROLE_ENABLED_USER] : [self::ROLE_DISABLED_USER];
28 |
29 | if ($user instanceof PremiumUser) {
30 | $roles[] = self::ROLE_PREMIUM_USER;
31 | }
32 |
33 | return $roles;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Security/UserChecker.php:
--------------------------------------------------------------------------------
1 | em = $em;
25 | $this->logger = $logger ?? new NullLogger();
26 | }
27 |
28 | public function checkPreAuth(UserInterface $identity): void
29 | {
30 | if (!$identity instanceof UserIdentity) {
31 | return;
32 | }
33 |
34 | if (null === $user = $this->em->find(User::class, $userId = $identity->getUserId())) {
35 | throw new AuthenticationCredentialsNotFoundException('Bad credentials.');
36 | }
37 |
38 | /** @var User $user */
39 | if (!$user->isEnabled()) {
40 | $this->logger->info('Disabled user login attempt.', ['id' => $userId->toString(), 'email' => $user->getEmail()]);
41 |
42 | throw new DisabledException('Bad credentials.');
43 | }
44 | }
45 |
46 | public function checkPostAuth(UserInterface $user): void
47 | {
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/symfony.lock:
--------------------------------------------------------------------------------
1 | {
2 | "api-platform/api-pack": {
3 | "version": "v1.2.1"
4 | },
5 | "api-platform/core": {
6 | "version": "2.5",
7 | "recipe": {
8 | "repo": "github.com/symfony/recipes",
9 | "branch": "master",
10 | "version": "2.5",
11 | "ref": "a93061567140e386f107be75340ac2aee3f86cbf"
12 | },
13 | "files": [
14 | "config/packages/api_platform.yaml",
15 | "config/routes/api_platform.yaml",
16 | "src/Entity/.gitignore"
17 | ]
18 | },
19 | "clue/stream-filter": {
20 | "version": "v1.4.1"
21 | },
22 | "doctrine/annotations": {
23 | "version": "1.0",
24 | "recipe": {
25 | "repo": "github.com/symfony/recipes",
26 | "branch": "master",
27 | "version": "1.0",
28 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457"
29 | },
30 | "files": [
31 | "config/routes/annotations.yaml"
32 | ]
33 | },
34 | "doctrine/cache": {
35 | "version": "1.10.0"
36 | },
37 | "doctrine/collections": {
38 | "version": "1.6.4"
39 | },
40 | "doctrine/common": {
41 | "version": "2.12.0"
42 | },
43 | "doctrine/data-fixtures": {
44 | "version": "1.4.2"
45 | },
46 | "doctrine/dbal": {
47 | "version": "v2.10.1"
48 | },
49 | "doctrine/doctrine-bundle": {
50 | "version": "2.0",
51 | "recipe": {
52 | "repo": "github.com/symfony/recipes",
53 | "branch": "master",
54 | "version": "2.0",
55 | "ref": "a9f2463b9f73efe74482f831f03a204a41328555"
56 | },
57 | "files": [
58 | "config/packages/doctrine.yaml",
59 | "config/packages/prod/doctrine.yaml",
60 | "src/Entity/.gitignore",
61 | "src/Repository/.gitignore"
62 | ]
63 | },
64 | "doctrine/doctrine-fixtures-bundle": {
65 | "version": "3.0",
66 | "recipe": {
67 | "repo": "github.com/symfony/recipes",
68 | "branch": "master",
69 | "version": "3.0",
70 | "ref": "fc52d86631a6dfd9fdf3381d0b7e3df2069e51b3"
71 | },
72 | "files": [
73 | "src/DataFixtures/AppFixtures.php"
74 | ]
75 | },
76 | "doctrine/doctrine-migrations-bundle": {
77 | "version": "1.2",
78 | "recipe": {
79 | "repo": "github.com/symfony/recipes",
80 | "branch": "master",
81 | "version": "1.2",
82 | "ref": "c1431086fec31f17fbcfe6d6d7e92059458facc1"
83 | },
84 | "files": [
85 | "config/packages/doctrine_migrations.yaml",
86 | "src/Migrations/.gitignore"
87 | ]
88 | },
89 | "doctrine/event-manager": {
90 | "version": "1.1.0"
91 | },
92 | "doctrine/inflector": {
93 | "version": "1.3.1"
94 | },
95 | "doctrine/instantiator": {
96 | "version": "1.3.0"
97 | },
98 | "doctrine/lexer": {
99 | "version": "1.2.0"
100 | },
101 | "doctrine/migrations": {
102 | "version": "2.2.1"
103 | },
104 | "doctrine/orm": {
105 | "version": "v2.7.1"
106 | },
107 | "doctrine/persistence": {
108 | "version": "1.3.6"
109 | },
110 | "doctrine/reflection": {
111 | "version": "v1.1.0"
112 | },
113 | "easycorp/easy-log-handler": {
114 | "version": "1.0",
115 | "recipe": {
116 | "repo": "github.com/symfony/recipes",
117 | "branch": "master",
118 | "version": "1.0",
119 | "ref": "70062abc2cd58794d2a90274502f81b55cd9951b"
120 | },
121 | "files": [
122 | "config/packages/dev/easy_log_handler.yaml"
123 | ]
124 | },
125 | "egulias/email-validator": {
126 | "version": "2.1.17"
127 | },
128 | "elasticsearch/elasticsearch": {
129 | "version": "v7.6.1"
130 | },
131 | "ezimuel/guzzlestreams": {
132 | "version": "3.0.1"
133 | },
134 | "ezimuel/ringphp": {
135 | "version": "1.1.2"
136 | },
137 | "fig/link-util": {
138 | "version": "1.1.0"
139 | },
140 | "guzzlehttp/guzzle": {
141 | "version": "6.5.2"
142 | },
143 | "guzzlehttp/promises": {
144 | "version": "v1.3.1"
145 | },
146 | "guzzlehttp/psr7": {
147 | "version": "1.6.1"
148 | },
149 | "hwi/oauth-bundle": {
150 | "version": "0.6",
151 | "recipe": {
152 | "repo": "github.com/symfony/recipes-contrib",
153 | "branch": "master",
154 | "version": "0.6",
155 | "ref": "20cacc9b2da49d96ea55c8a8dd31324c5be88bc9"
156 | },
157 | "files": [
158 | "config/packages/hwi_oauth.yaml",
159 | "config/routes/hwi_oauth_routing.yaml"
160 | ]
161 | },
162 | "jdorn/sql-formatter": {
163 | "version": "v1.2.17"
164 | },
165 | "laminas/laminas-code": {
166 | "version": "3.4.1"
167 | },
168 | "laminas/laminas-eventmanager": {
169 | "version": "3.2.1"
170 | },
171 | "laminas/laminas-zendframework-bridge": {
172 | "version": "1.0.1"
173 | },
174 | "lcobucci/jwt": {
175 | "version": "3.3.1"
176 | },
177 | "lexik/jwt-authentication-bundle": {
178 | "version": "2.5",
179 | "recipe": {
180 | "repo": "github.com/symfony/recipes",
181 | "branch": "master",
182 | "version": "2.5",
183 | "ref": "5b2157bcd5778166a5696e42f552ad36529a07a6"
184 | },
185 | "files": [
186 | "config/packages/lexik_jwt_authentication.yaml"
187 | ]
188 | },
189 | "monolog/monolog": {
190 | "version": "2.0.2"
191 | },
192 | "moontoast/math": {
193 | "version": "1.2.1"
194 | },
195 | "msgphp/domain": {
196 | "version": "v0.15.0"
197 | },
198 | "msgphp/eav": {
199 | "version": "v0.15.0"
200 | },
201 | "msgphp/eav-bundle": {
202 | "version": "0.10",
203 | "recipe": {
204 | "repo": "github.com/symfony/recipes-contrib",
205 | "branch": "master",
206 | "version": "0.10",
207 | "ref": "9b2774120df769609554a5883df6c3a2224a9761"
208 | },
209 | "files": [
210 | "config/packages/msgphp_eav.php",
211 | "src/Entity/Attribute.php",
212 | "src/Entity/AttributeValue.php"
213 | ]
214 | },
215 | "msgphp/user": {
216 | "version": "v0.15.0"
217 | },
218 | "msgphp/user-bundle": {
219 | "version": "0.10",
220 | "recipe": {
221 | "repo": "github.com/symfony/recipes-contrib",
222 | "branch": "master",
223 | "version": "0.10",
224 | "ref": "4067ee796789a8828ec72f82c76508a76875bcf8"
225 | },
226 | "files": [
227 | "config/packages/msgphp_user.php",
228 | "src/Entity/User.php"
229 | ]
230 | },
231 | "msgphp/user-eav": {
232 | "version": "v0.15.0"
233 | },
234 | "namshi/jose": {
235 | "version": "7.2.3"
236 | },
237 | "nelmio/cors-bundle": {
238 | "version": "1.5",
239 | "recipe": {
240 | "repo": "github.com/symfony/recipes",
241 | "branch": "master",
242 | "version": "1.5",
243 | "ref": "6388de23860284db9acce0a7a5d9d13153bcb571"
244 | },
245 | "files": [
246 | "config/packages/nelmio_cors.yaml"
247 | ]
248 | },
249 | "nikic/php-parser": {
250 | "version": "v4.3.0"
251 | },
252 | "ocramius/package-versions": {
253 | "version": "1.5.1"
254 | },
255 | "ocramius/proxy-manager": {
256 | "version": "2.2.3"
257 | },
258 | "pascaldevink/shortuuid": {
259 | "version": "2.2.0"
260 | },
261 | "php": {
262 | "version": "7.3"
263 | },
264 | "php-http/client-common": {
265 | "version": "2.1.0"
266 | },
267 | "php-http/discovery": {
268 | "version": "1.7.4"
269 | },
270 | "php-http/guzzle6-adapter": {
271 | "version": "v2.0.1"
272 | },
273 | "php-http/httplug": {
274 | "version": "2.1.0"
275 | },
276 | "php-http/httplug-bundle": {
277 | "version": "1.6",
278 | "recipe": {
279 | "repo": "github.com/symfony/recipes-contrib",
280 | "branch": "master",
281 | "version": "1.6",
282 | "ref": "87b3d491d593d2c6e3b90bf0689ac69a188bfa5f"
283 | },
284 | "files": [
285 | "config/packages/httplug.yaml"
286 | ]
287 | },
288 | "php-http/logger-plugin": {
289 | "version": "1.1.0"
290 | },
291 | "php-http/message": {
292 | "version": "1.8.0"
293 | },
294 | "php-http/message-factory": {
295 | "version": "v1.0.2"
296 | },
297 | "php-http/promise": {
298 | "version": "v1.0.0"
299 | },
300 | "php-http/stopwatch-plugin": {
301 | "version": "1.3.0"
302 | },
303 | "phpdocumentor/reflection-common": {
304 | "version": "2.0.0"
305 | },
306 | "phpdocumentor/reflection-docblock": {
307 | "version": "4.3.4"
308 | },
309 | "phpdocumentor/type-resolver": {
310 | "version": "1.1.0"
311 | },
312 | "phpseclib/bcmath_compat": {
313 | "version": "1.0.4"
314 | },
315 | "phpseclib/phpseclib": {
316 | "version": "2.0.25"
317 | },
318 | "psr/cache": {
319 | "version": "1.0.1"
320 | },
321 | "psr/container": {
322 | "version": "1.0.0"
323 | },
324 | "psr/event-dispatcher": {
325 | "version": "1.0.0"
326 | },
327 | "psr/http-client": {
328 | "version": "1.0.0"
329 | },
330 | "psr/http-message": {
331 | "version": "1.0.1"
332 | },
333 | "psr/link": {
334 | "version": "1.0.0"
335 | },
336 | "psr/log": {
337 | "version": "1.1.2"
338 | },
339 | "ralouphie/getallheaders": {
340 | "version": "3.0.3"
341 | },
342 | "ramsey/uuid": {
343 | "version": "3.9.3"
344 | },
345 | "ramsey/uuid-doctrine": {
346 | "version": "1.3",
347 | "recipe": {
348 | "repo": "github.com/symfony/recipes-contrib",
349 | "branch": "master",
350 | "version": "1.3",
351 | "ref": "471aed0fbf5620b8d7f92b7a5ebbbf6c0945c27a"
352 | },
353 | "files": [
354 | "config/packages/ramsey_uuid_doctrine.yaml"
355 | ]
356 | },
357 | "react/promise": {
358 | "version": "v2.7.1"
359 | },
360 | "sensio/framework-extra-bundle": {
361 | "version": "5.2",
362 | "recipe": {
363 | "repo": "github.com/symfony/recipes",
364 | "branch": "master",
365 | "version": "5.2",
366 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b"
367 | },
368 | "files": [
369 | "config/packages/sensio_framework_extra.yaml"
370 | ]
371 | },
372 | "symfony/asset": {
373 | "version": "v5.0.5"
374 | },
375 | "symfony/browser-kit": {
376 | "version": "v5.0.5"
377 | },
378 | "symfony/cache": {
379 | "version": "v5.0.5"
380 | },
381 | "symfony/cache-contracts": {
382 | "version": "v2.0.1"
383 | },
384 | "symfony/config": {
385 | "version": "v5.0.5"
386 | },
387 | "symfony/console": {
388 | "version": "4.4",
389 | "recipe": {
390 | "repo": "github.com/symfony/recipes",
391 | "branch": "master",
392 | "version": "4.4",
393 | "ref": "ea8c0eda34fda57e7d5cd8cbd889e2a387e3472c"
394 | },
395 | "files": [
396 | "bin/console",
397 | "config/bootstrap.php"
398 | ]
399 | },
400 | "symfony/css-selector": {
401 | "version": "v5.0.5"
402 | },
403 | "symfony/debug-bundle": {
404 | "version": "4.1",
405 | "recipe": {
406 | "repo": "github.com/symfony/recipes",
407 | "branch": "master",
408 | "version": "4.1",
409 | "ref": "f8863cbad2f2e58c4b65fa1eac892ab189971bea"
410 | },
411 | "files": [
412 | "config/packages/dev/debug.yaml"
413 | ]
414 | },
415 | "symfony/debug-pack": {
416 | "version": "v1.0.7"
417 | },
418 | "symfony/dependency-injection": {
419 | "version": "v5.0.5"
420 | },
421 | "symfony/doctrine-bridge": {
422 | "version": "v5.0.5"
423 | },
424 | "symfony/dom-crawler": {
425 | "version": "v5.0.5"
426 | },
427 | "symfony/dotenv": {
428 | "version": "v5.0.5"
429 | },
430 | "symfony/error-handler": {
431 | "version": "v5.0.5"
432 | },
433 | "symfony/event-dispatcher": {
434 | "version": "v5.0.5"
435 | },
436 | "symfony/event-dispatcher-contracts": {
437 | "version": "v2.0.1"
438 | },
439 | "symfony/expression-language": {
440 | "version": "v5.0.5"
441 | },
442 | "symfony/filesystem": {
443 | "version": "v5.0.5"
444 | },
445 | "symfony/finder": {
446 | "version": "v5.0.5"
447 | },
448 | "symfony/flex": {
449 | "version": "1.0",
450 | "recipe": {
451 | "repo": "github.com/symfony/recipes",
452 | "branch": "master",
453 | "version": "1.0",
454 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e"
455 | },
456 | "files": [
457 | ".env"
458 | ]
459 | },
460 | "symfony/form": {
461 | "version": "v5.0.5"
462 | },
463 | "symfony/framework-bundle": {
464 | "version": "4.4",
465 | "recipe": {
466 | "repo": "github.com/symfony/recipes",
467 | "branch": "master",
468 | "version": "4.4",
469 | "ref": "23ecaccc551fe2f74baf613811ae529eb07762fa"
470 | },
471 | "files": [
472 | "config/bootstrap.php",
473 | "config/packages/cache.yaml",
474 | "config/packages/framework.yaml",
475 | "config/packages/test/framework.yaml",
476 | "config/routes/dev/framework.yaml",
477 | "config/services.yaml",
478 | "public/index.php",
479 | "src/Controller/.gitignore",
480 | "src/Kernel.php"
481 | ]
482 | },
483 | "symfony/http-foundation": {
484 | "version": "v5.0.5"
485 | },
486 | "symfony/http-kernel": {
487 | "version": "v5.0.5"
488 | },
489 | "symfony/inflector": {
490 | "version": "v5.0.5"
491 | },
492 | "symfony/intl": {
493 | "version": "v5.0.5"
494 | },
495 | "symfony/mailer": {
496 | "version": "4.3",
497 | "recipe": {
498 | "repo": "github.com/symfony/recipes",
499 | "branch": "master",
500 | "version": "4.3",
501 | "ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2"
502 | },
503 | "files": [
504 | "config/packages/mailer.yaml"
505 | ]
506 | },
507 | "symfony/maker-bundle": {
508 | "version": "1.0",
509 | "recipe": {
510 | "repo": "github.com/symfony/recipes",
511 | "branch": "master",
512 | "version": "1.0",
513 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
514 | }
515 | },
516 | "symfony/messenger": {
517 | "version": "4.3",
518 | "recipe": {
519 | "repo": "github.com/symfony/recipes",
520 | "branch": "master",
521 | "version": "4.3",
522 | "ref": "8a2675c061737658bed85102e9241c752620e575"
523 | },
524 | "files": [
525 | "config/packages/messenger.yaml"
526 | ]
527 | },
528 | "symfony/mime": {
529 | "version": "v5.0.5"
530 | },
531 | "symfony/monolog-bridge": {
532 | "version": "v5.0.5"
533 | },
534 | "symfony/monolog-bundle": {
535 | "version": "3.3",
536 | "recipe": {
537 | "repo": "github.com/symfony/recipes",
538 | "branch": "master",
539 | "version": "3.3",
540 | "ref": "877bdb4223245783d00ed1f7429aa7ebc606d914"
541 | },
542 | "files": [
543 | "config/packages/dev/monolog.yaml",
544 | "config/packages/prod/monolog.yaml",
545 | "config/packages/test/monolog.yaml"
546 | ]
547 | },
548 | "symfony/options-resolver": {
549 | "version": "v5.0.5"
550 | },
551 | "symfony/orm-pack": {
552 | "version": "v1.0.8"
553 | },
554 | "symfony/phpunit-bridge": {
555 | "version": "4.3",
556 | "recipe": {
557 | "repo": "github.com/symfony/recipes",
558 | "branch": "master",
559 | "version": "4.3",
560 | "ref": "6d0e35f749d5f4bfe1f011762875275cd3f9874f"
561 | },
562 | "files": [
563 | ".env.test",
564 | "bin/phpunit",
565 | "phpunit.xml.dist",
566 | "tests/bootstrap.php"
567 | ]
568 | },
569 | "symfony/polyfill-intl-icu": {
570 | "version": "v1.14.0"
571 | },
572 | "symfony/polyfill-intl-idn": {
573 | "version": "v1.14.0"
574 | },
575 | "symfony/polyfill-mbstring": {
576 | "version": "v1.14.0"
577 | },
578 | "symfony/profiler-pack": {
579 | "version": "v1.0.4"
580 | },
581 | "symfony/property-access": {
582 | "version": "v5.0.5"
583 | },
584 | "symfony/property-info": {
585 | "version": "v5.0.5"
586 | },
587 | "symfony/routing": {
588 | "version": "4.2",
589 | "recipe": {
590 | "repo": "github.com/symfony/recipes",
591 | "branch": "master",
592 | "version": "4.2",
593 | "ref": "683dcb08707ba8d41b7e34adb0344bfd68d248a7"
594 | },
595 | "files": [
596 | "config/packages/prod/routing.yaml",
597 | "config/packages/routing.yaml",
598 | "config/routes.yaml"
599 | ]
600 | },
601 | "symfony/security-bundle": {
602 | "version": "4.4",
603 | "recipe": {
604 | "repo": "github.com/symfony/recipes",
605 | "branch": "master",
606 | "version": "4.4",
607 | "ref": "7b4408dc203049666fe23fabed23cbadc6d8440f"
608 | },
609 | "files": [
610 | "config/packages/security.yaml"
611 | ]
612 | },
613 | "symfony/security-core": {
614 | "version": "v5.0.5"
615 | },
616 | "symfony/security-csrf": {
617 | "version": "v5.0.5"
618 | },
619 | "symfony/security-guard": {
620 | "version": "v5.0.5"
621 | },
622 | "symfony/security-http": {
623 | "version": "v5.0.5"
624 | },
625 | "symfony/serializer": {
626 | "version": "v5.0.5"
627 | },
628 | "symfony/service-contracts": {
629 | "version": "v2.0.1"
630 | },
631 | "symfony/stopwatch": {
632 | "version": "v5.0.5"
633 | },
634 | "symfony/templating": {
635 | "version": "v5.0.5"
636 | },
637 | "symfony/test-pack": {
638 | "version": "v1.0.6"
639 | },
640 | "symfony/translation": {
641 | "version": "3.3",
642 | "recipe": {
643 | "repo": "github.com/symfony/recipes",
644 | "branch": "master",
645 | "version": "3.3",
646 | "ref": "2ad9d2545bce8ca1a863e50e92141f0b9d87ffcd"
647 | },
648 | "files": [
649 | "config/packages/translation.yaml",
650 | "translations/.gitignore"
651 | ]
652 | },
653 | "symfony/translation-contracts": {
654 | "version": "v2.0.1"
655 | },
656 | "symfony/twig-bridge": {
657 | "version": "v5.0.5"
658 | },
659 | "symfony/twig-bundle": {
660 | "version": "5.0",
661 | "recipe": {
662 | "repo": "github.com/symfony/recipes",
663 | "branch": "master",
664 | "version": "5.0",
665 | "ref": "fab9149bbaa4d5eca054ed93f9e1b66cc500895d"
666 | },
667 | "files": [
668 | "config/packages/test/twig.yaml",
669 | "config/packages/twig.yaml",
670 | "templates/base.html.twig"
671 | ]
672 | },
673 | "symfony/validator": {
674 | "version": "4.3",
675 | "recipe": {
676 | "repo": "github.com/symfony/recipes",
677 | "branch": "master",
678 | "version": "4.3",
679 | "ref": "d902da3e4952f18d3bf05aab29512eb61cabd869"
680 | },
681 | "files": [
682 | "config/packages/test/validator.yaml",
683 | "config/packages/validator.yaml"
684 | ]
685 | },
686 | "symfony/var-dumper": {
687 | "version": "v5.0.5"
688 | },
689 | "symfony/var-exporter": {
690 | "version": "v5.0.5"
691 | },
692 | "symfony/web-link": {
693 | "version": "v5.0.5"
694 | },
695 | "symfony/web-profiler-bundle": {
696 | "version": "3.3",
697 | "recipe": {
698 | "repo": "github.com/symfony/recipes",
699 | "branch": "master",
700 | "version": "3.3",
701 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6"
702 | },
703 | "files": [
704 | "config/packages/dev/web_profiler.yaml",
705 | "config/packages/test/web_profiler.yaml",
706 | "config/routes/dev/web_profiler.yaml"
707 | ]
708 | },
709 | "symfony/yaml": {
710 | "version": "v5.0.5"
711 | },
712 | "twig/twig": {
713 | "version": "v3.0.3"
714 | },
715 | "webmozart/assert": {
716 | "version": "1.7.0"
717 | },
718 | "webonyx/graphql-php": {
719 | "version": "v0.13.8"
720 | },
721 | "willdurand/negotiation": {
722 | "version": "v2.3.1"
723 | }
724 | }
725 |
--------------------------------------------------------------------------------
/templates/base.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}Welcome!{% endblock %}
6 | {% block stylesheets %}{% endblock %}
7 |
8 |
9 | {% block header %}
10 |
11 | ★ The App
12 |
33 |
34 | {% endblock %}
35 | {% block body %}
36 | {{ include('partials/flash-messages.html.twig') }}
37 | {% block main %}{% endblock %}
38 | {% endblock %}
39 | {% block javascripts %}{% endblock %}
40 |
41 |
42 |
--------------------------------------------------------------------------------
/templates/email.html.twig:
--------------------------------------------------------------------------------
1 | ★ The App
2 |
3 | {% block body %}{% endblock %}
4 |
5 | Cheers! The App,
6 |
--------------------------------------------------------------------------------
/templates/email.txt.twig:
--------------------------------------------------------------------------------
1 | ★ The App
2 | =========
3 |
4 | {% block body %}{% endblock %}
5 |
6 | --
7 |
8 | Cheers! The App,
9 |
--------------------------------------------------------------------------------
/templates/home.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% block main %}
4 | Hello 🌍
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/templates/partials/flash-messages.html.twig:
--------------------------------------------------------------------------------
1 | {% if app.request.hasPreviousSession %}
2 |
3 | {% for type, messages in app.flashes %}
4 | {% for message in messages %}
5 | - [{{ type }}] {{ message|trans }}
6 | {% endfor %}
7 | {% endfor %}
8 |
9 | {% endif %}
10 |
--------------------------------------------------------------------------------
/templates/partials/oauth/login_entrypoint.html.twig:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/templates/password_confirmation.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% block main %}
4 | Confirm your password to proceed.
5 |
6 | {{ form_start(form) }}
7 | {{ form_errors(form) }}
8 | {{ form_row(form.password.plain, {label: 'Password', translation_domain: false}) }}
9 |
10 |
13 | {{ form_end(form) }}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/templates/user/email/confirm_email.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'email.html.twig' %}
2 |
3 | {% block body %}
4 | {% set url = url('confirm_email', {token: userEmail.confirmationToken}) %}
5 | Confirm your e-mail at {{ url }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/templates/user/email/confirm_email.txt.twig:
--------------------------------------------------------------------------------
1 | {% extends 'email.txt.twig' %}
2 |
3 | {% block body %}
4 | Confirm your e-mail at {{ url('confirm_email', {token: userEmail.confirmationToken}) }}
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/templates/user/email/confirm_registration.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'email.html.twig' %}
2 |
3 | {% block body %}
4 | {% set url = url('confirm_registration', {token: user.confirmationToken}) %}
5 | Confirm your account at {{ url }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/templates/user/email/confirm_registration.txt.twig:
--------------------------------------------------------------------------------
1 | {% extends 'email.txt.twig' %}
2 |
3 | {% block body %}
4 | Confirm your account at {{ url('confirm_registration', {token: user.confirmationToken}) }}
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/templates/user/email/invited.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'email.html.twig' %}
2 |
3 | {% block body %}
4 | {% set url = url('register', {token: invitation.token}) %}
5 | Complete your registration at {{ url }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/templates/user/email/invited.txt.twig:
--------------------------------------------------------------------------------
1 | {% extends 'email.txt.twig' %}
2 |
3 | {% block body %}
4 | Complete your registration at {{ url('register', {token: invitation.token}) }}
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/templates/user/email/reset_password.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'email.html.twig' %}
2 |
3 | {% block body %}
4 | {% set url = url('reset_password', {token: user.passwordResetToken}) %}
5 | Reset your password at {{ url }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/templates/user/email/reset_password.txt.twig:
--------------------------------------------------------------------------------
1 | {% extends 'email.txt.twig' %}
2 |
3 | {% block body %}
4 | Reset your password at {{ url('reset_password', {token: user.passwordResetToken}) }}
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/templates/user/forgot_password.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% block main %}
4 | Forgot Your Password?
5 |
6 | {{ form_start(form) }}
7 | {{ form_errors(form) }}
8 | {{ form_row(form.email, {label: 'E-mail', translation_domain: false}) }}
9 |
10 |
11 |
12 |
13 | {{ form_end(form) }}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/templates/user/login.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% block main %}
4 | Login
5 |
6 | {% if error %}
7 | {{ error.messageKey|trans(error.messageData, 'security') }}
8 | {% endif %}
9 |
10 | {{ form_start(form) }}
11 | {{ form_errors(form) }}
12 | {{ form_row(form.email, {label: 'E-mail', translation_domain: false}) }}
13 | {{ form_row(form.password, {label: 'Password', translation_domain: false}) }}
14 | {{ form_row(form.remember_me, {label: 'Remember me', required: false, translation_domain: false}) }}
15 |
16 |
20 | {{ form_end(form) }}
21 |
22 | Or Login Using:
23 |
24 | {% include 'partials/oauth/login_entrypoint.html.twig' %}
25 |
26 | Got a One-Time-Login Token?
27 |
28 | {{ form_start(oneTimeLoginForm) }}
29 | {{ form_errors(oneTimeLoginForm) }}
30 | {{ form_row(oneTimeLoginForm.token, {label: 'Your token', translation_domain: false}) }}
31 |
32 |
33 |
34 |
35 | {{ form_end(oneTimeLoginForm) }}
36 | {% endblock %}
37 |
--------------------------------------------------------------------------------
/templates/user/profile.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% set userId, user = msgphp_user.currentId, msgphp_user.current %}
4 | {% set oauth_resources = {
5 | (constant('App\\Entity\\Attribute::GOOGLE_OAUTH_ID')): 'Google',
6 | (constant('App\\Entity\\Attribute::FACEBOOK_OAUTH_ID')): 'Facebook'
7 | } %}
8 |
9 | {% block main %}
10 | My Profile
11 |
12 |
39 |
40 |
46 |
47 |
75 |
76 |
89 | {% endblock %}
90 |
--------------------------------------------------------------------------------
/templates/user/register.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% block main %}
4 | Register
5 |
6 | {{ form_start(form) }}
7 | {{ form_errors(form) }}
8 | {{ form_row(form.email, {label: 'E-mail', translation_domain: false}) }}
9 | {{ form_row(form.password.plain, {label: 'Password', translation_domain: false}) }}
10 | {{ form_row(form.password.confirmation, {label: 'Confirm password', translation_domain: false}) }}
11 |
12 |
13 |
14 |
15 | {{ form_end(form) }}
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/templates/user/reset_password.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% block main %}
4 | Reset Your Password
5 |
6 | {{ form_start(form) }}
7 | {{ form_errors(form) }}
8 | {{ form_row(form.password.plain, {label: 'New password', translation_domain: false}) }}
9 | {{ form_row(form.password.confirmation, {label: 'Confirm new password', translation_domain: false}) }}
10 |
11 |
12 |
13 |
14 | {{ form_end(form) }}
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | bootEnv(dirname(__DIR__).'/.env');
13 | }
14 |
--------------------------------------------------------------------------------
/translations/messages+intl-icu.en.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | role.ROLE_USER
8 | Regular User
9 |
10 |
11 | role.ROLE_DISABLED_USER
12 | Inactive User
13 |
14 |
15 | role.ROLE_ENABLED_USER
16 | Active User
17 |
18 |
19 | role.ROLE_PREMIUM_USER
20 | Premium User
21 |
22 |
23 | role.ROLE_ADMIN
24 | Administrator
25 |
26 |
27 |
28 |
29 | oauth.resource_owner.facebook
30 | Facebook
31 |
32 |
33 | oauth.resource_owner.google
34 | Google
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------