Skip to content

Commit 22b829f

Browse files
authored
Merge pull request #28 from moufmouf/login_action
Adding login action
2 parents 8c44ae3 + 5cd8d7a commit 22b829f

File tree

10 files changed

+311
-35
lines changed

10 files changed

+311
-35
lines changed

Controller/GraphQL/InvalidUserPasswordException.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
namespace TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL;
55

66

7-
class InvalidUserPasswordException
8-
{
7+
use Exception;
8+
use TheCodingMachine\GraphQLite\GraphQLException;
99

10+
class InvalidUserPasswordException extends GraphQLException
11+
{
12+
public static function create(Exception $previous = null)
13+
{
14+
return new self('The provided user / password is incorrect.', 401, $previous);
15+
}
1016
}
Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,100 @@
11
<?php
2-
3-
42
namespace TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL;
53

64

5+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
8+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
9+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
10+
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
11+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
12+
use Symfony\Component\Security\Core\User\UserProviderInterface;
13+
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
14+
use TheCodingMachine\GraphQLite\Annotations\Mutation;
15+
use TheCodingMachine\GraphQLite\Annotations\Query;
16+
717
class LoginController
818
{
919

10-
}
20+
/**
21+
* @var UserProviderInterface
22+
*/
23+
private $userProvider;
24+
/**
25+
* @var UserPasswordEncoderInterface
26+
*/
27+
private $passwordEncoder;
28+
/**
29+
* @var TokenStorageInterface
30+
*/
31+
private $tokenStorage;
32+
/**
33+
* @var string
34+
*/
35+
private $firewallName;
36+
/**
37+
* @var SessionInterface
38+
*/
39+
private $session;
40+
/**
41+
* @var EventDispatcherInterface
42+
*/
43+
private $eventDispatcher;
44+
45+
public function __construct(UserProviderInterface $userProvider, UserPasswordEncoderInterface $passwordEncoder, TokenStorageInterface $tokenStorage, SessionInterface $session, EventDispatcherInterface $eventDispatcher, string $firewallName)
46+
{
47+
$this->userProvider = $userProvider;
48+
$this->passwordEncoder = $passwordEncoder;
49+
$this->tokenStorage = $tokenStorage;
50+
$this->firewallName = $firewallName;
51+
$this->session = $session;
52+
$this->eventDispatcher = $eventDispatcher;
53+
}
54+
55+
/**
56+
* @Mutation()
57+
*/
58+
public function login(string $userName, string $password, Request $request): bool
59+
{
60+
try {
61+
$user = $this->userProvider->loadUserByUsername($userName);
62+
} catch (UsernameNotFoundException $e) {
63+
// FIXME: should we return false instead???
64+
throw InvalidUserPasswordException::create($e);
65+
}
66+
67+
if (!$this->passwordEncoder->isPasswordValid($user, $password)) {
68+
throw InvalidUserPasswordException::create();
69+
}
70+
71+
// User and passwords are valid. Let's login!
72+
73+
// Handle getting or creating the user entity likely with a posted form
74+
// The third parameter "main" can change according to the name of your firewall in security.yml
75+
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
76+
$this->tokenStorage->setToken($token);
77+
78+
// If the firewall name is not main, then the set value would be instead:
79+
// $this->get('session')->set('_security_XXXFIREWALLNAMEXXX', serialize($token));
80+
$this->session->set('_security_'.$this->firewallName, serialize($token));
81+
82+
// Fire the login event manually
83+
$event = new InteractiveLoginEvent($request, $token);
84+
$this->eventDispatcher->dispatch($event, 'security.interactive_login');
85+
86+
return true;
87+
}
88+
89+
/**
90+
* @Mutation()
91+
*/
92+
public function logout(): bool
93+
{
94+
$this->tokenStorage->setToken(null);
95+
96+
$this->session->remove('_security_'.$this->firewallName);
97+
98+
return true;
99+
}
100+
}

DependencyInjection/Configuration.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public function getConfigTreeBuilder()
3737
->booleanNode('RETHROW_UNSAFE_EXCEPTIONS')->defaultTrue()->info('Exceptions that do not implement ClientAware interface are not caught by the engine and propagated to Symfony.')->end()
3838
->end()
3939
->end()
40+
->arrayNode('security')
41+
->children()
42+
->enumNode('enable_login')->values(['on', 'off', 'auto'])->defaultValue('auto')->info('Enable to automatically create a login/logout mutation. "on": enable, "auto": enable if security bundle is available.')->end()
43+
->scalarNode('firewall_name')->defaultValue('main')->info('The name of the firewall to use for login')->end()
44+
->end()
45+
->end()
4046
;
4147

4248
return $treeBuilder;

DependencyInjection/GraphqliteCompilerPass.php

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,26 @@
2727
use Symfony\Component\DependencyInjection\ContainerBuilder;
2828
use Symfony\Component\DependencyInjection\Definition;
2929
use Symfony\Component\DependencyInjection\Reference;
30+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
31+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
32+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
33+
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
34+
use Symfony\Component\Security\Core\User\UserProviderInterface;
3035
use TheCodingMachine\CacheUtils\ClassBoundCache;
3136
use TheCodingMachine\CacheUtils\ClassBoundCacheContract;
3237
use TheCodingMachine\CacheUtils\ClassBoundCacheContractInterface;
3338
use TheCodingMachine\CacheUtils\ClassBoundMemoryAdapter;
3439
use TheCodingMachine\CacheUtils\FileBoundCache;
3540
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
41+
use TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory;
3642
use TheCodingMachine\GraphQLite\AnnotationReader;
3743
use TheCodingMachine\GraphQLite\Annotations\AbstractRequest;
3844
use TheCodingMachine\GraphQLite\Annotations\Autowire;
3945
use TheCodingMachine\GraphQLite\Annotations\Field;
4046
use TheCodingMachine\GraphQLite\Annotations\Mutation;
4147
use TheCodingMachine\GraphQLite\Annotations\Parameter;
4248
use TheCodingMachine\GraphQLite\Annotations\Query;
49+
use TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController;
4350
use TheCodingMachine\GraphQLite\FieldsBuilder;
4451
use TheCodingMachine\GraphQLite\FieldsBuilderFactory;
4552
use TheCodingMachine\GraphQLite\GraphQLException;
@@ -80,24 +87,54 @@ public function process(ContainerBuilder $container)
8087
$typesNamespaces = $container->getParameter('graphqlite.namespace.types');
8188

8289
// 2 seconds of TTL in environment mode. Otherwise, let's cache forever!
90+
91+
$schemaFactory = $container->getDefinition(SchemaFactory::class);
92+
8393
$env = $container->getParameter('kernel.environment');
84-
$globTtl = null;
85-
if ($env === 'dev') {
86-
$globTtl = 2;
94+
if ($env === 'prod') {
95+
$schemaFactory->addMethodCall('prodMode');
96+
} elseif ($env === 'dev') {
97+
$schemaFactory->addMethodCall('devMode');
98+
}
99+
100+
$disableLogin = false;
101+
if ($container->getParameter('graphqlite.security.enable_login') === 'auto'
102+
&& (!$container->has(UserProviderInterface::class) ||
103+
!$container->has(UserPasswordEncoderInterface::class) ||
104+
!$container->has(TokenStorageInterface::class) ||
105+
!$container->has(SessionInterface::class)
106+
)) {
107+
$disableLogin = true;
108+
}
109+
if ($container->getParameter('graphqlite.security.enable_login') === 'off') {
110+
$disableLogin = true;
111+
}
112+
// If the security is disabled, let's remove the LoginController
113+
if ($disableLogin === true) {
114+
$container->removeDefinition(LoginController::class);
115+
$container->removeDefinition(AggregateControllerQueryProviderFactory::class);
116+
}
117+
118+
if ($container->getParameter('graphqlite.security.enable_login') === 'on') {
119+
if (!$container->has(SessionInterface::class)) {
120+
throw new GraphQLException('In order to enable the login/logout mutations (via the graphqlite.security.enable_login parameter), you need to enable session support (via the "framework.session.enabled" config parameter).');
121+
}
122+
if (!$container->has(UserPasswordEncoderInterface::class) || !$container->has(TokenStorageInterface::class) || !$container->has(UserProviderInterface::class)) {
123+
throw new GraphQLException('In order to enable the login/logout mutations (via the graphqlite.security.enable_login parameter), you need to install the security bundle. Please be sure to correctly configure the user provider (in the security.providers configuration settings)');
124+
}
87125
}
88126

89-
$schemaFactory = $container->getDefinition(SchemaFactory::class);
90127

91128
foreach ($container->getDefinitions() as $id => $definition) {
92129
if ($definition->isAbstract() || $definition->getClass() === null) {
93130
continue;
94131
}
95132
$class = $definition->getClass();
96-
foreach ($controllersNamespaces as $controllersNamespace) {
133+
/* foreach ($controllersNamespaces as $controllersNamespace) {
97134
if (strpos($class, $controllersNamespace) === 0) {
98135
$definition->addTag('graphql.annotated.controller');
99136
}
100-
}
137+
}*/
101138

102139
foreach ($typesNamespaces as $typesNamespace) {
103140
if (strpos($class, $typesNamespace) === 0) {
@@ -165,10 +202,12 @@ public function process(ContainerBuilder $container)
165202

166203
// Register graphql.queryprovider
167204
$this->mapAdderToTag('graphql.queryprovider', 'addQueryProvider', $container, $schemaFactory);
205+
$this->mapAdderToTag('graphql.queryprovider_factory', 'addQueryProviderFactory', $container, $schemaFactory);
168206
$this->mapAdderToTag('graphql.root_type_mapper', 'addRootTypeMapper', $container, $schemaFactory);
169207
$this->mapAdderToTag('graphql.parameter_mapper', 'addParameterMapper', $container, $schemaFactory);
170208
$this->mapAdderToTag('graphql.field_middleware', 'addFieldMiddleware', $container, $schemaFactory);
171209
$this->mapAdderToTag('graphql.type_mapper', 'addTypeMapper', $container, $schemaFactory);
210+
$this->mapAdderToTag('graphql.type_mapper_factory', 'addTypeMapperFactory', $container, $schemaFactory);
172211
}
173212

174213
/**

DependencyInjection/GraphqliteExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ public function load(array $configs, ContainerBuilder $container)
5454
$namespaceType = [];
5555
}
5656

57+
$enableLogin = $configs[0]['security']['enable_login'] ?? 'auto';
58+
5759
$container->setParameter('graphqlite.namespace.controllers', $namespaceController);
5860
$container->setParameter('graphqlite.namespace.types', $namespaceType);
61+
$container->setParameter('graphqlite.security.enable_login', $enableLogin);
62+
$container->setParameter('graphqlite.security.firewall_name', $configs[0]['security']['firewall_name'] ?? 'main');
5963

6064
$loader->load('graphqlite.xml');
6165

Resources/config/container/graphqlite.xml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
/>
2929
</service>
3030

31+
<service id="TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory">
32+
<argument type="collection">
33+
<argument>TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController</argument>
34+
</argument>
35+
<tag name="graphql.queryprovider_factory" />
36+
</service>
37+
3138
<service id="GraphQL\Type\Schema" alias="TheCodingMachine\GraphQLite\Schema" />
3239

3340

@@ -62,6 +69,13 @@
6269
<service id="TheCodingMachine\Graphqlite\Bundle\Mappers\RequestParameterMapper">
6370
<tag name="graphql.parameter_mapper"/>
6471
</service>
65-
</services>
6672

67-
</container>
73+
<service id="TheCodingMachine\Graphqlite\Bundle\Mappers\RequestParameterMapper">
74+
<tag name="graphql.parameter_mapper"/>
75+
</service>
76+
77+
<service id="TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController" public="true">
78+
<argument key="$firewallName">%graphqlite.security.firewall_name%</argument>
79+
</service>
80+
</services>
81+
</container>

Tests/Fixtures/config/services.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ services:
1919

2020
someService:
2121
class: stdClass
22+
23+
Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler:
24+
class: Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler
2225
# controllers are imported separately to make sure services can be injected
2326
# as action arguments even if you don't extend any base controller class
2427
#App\Controller\:

0 commit comments

Comments
 (0)