Skip to content

Commit d9e91ea

Browse files
committed
Add a "me" query and modify login return type
Now that the "@type" annotation is supported in the interfaces, we can create a "me" query and modify the return type of the "login" query to return a `UserInterface`!
1 parent 0cd551b commit d9e91ea

File tree

9 files changed

+254
-8
lines changed

9 files changed

+254
-8
lines changed

Controller/GraphQL/LoginController.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
1010
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
1111
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
12+
use Symfony\Component\Security\Core\User\UserInterface;
1213
use Symfony\Component\Security\Core\User\UserProviderInterface;
1314
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
1415
use TheCodingMachine\GraphQLite\Annotations\Mutation;
@@ -55,12 +56,12 @@ public function __construct(UserProviderInterface $userProvider, UserPasswordEnc
5556
/**
5657
* @Mutation()
5758
*/
58-
public function login(string $userName, string $password, Request $request): bool
59+
public function login(string $userName, string $password, Request $request): UserInterface
5960
{
6061
try {
6162
$user = $this->userProvider->loadUserByUsername($userName);
6263
} catch (UsernameNotFoundException $e) {
63-
// FIXME: should we return false instead???
64+
// FIXME: should we return null instead???
6465
throw InvalidUserPasswordException::create($e);
6566
}
6667

@@ -83,7 +84,7 @@ public function login(string $userName, string $password, Request $request): boo
8384
$event = new InteractiveLoginEvent($request, $token);
8485
$this->eventDispatcher->dispatch($event, 'security.interactive_login');
8586

86-
return true;
87+
return $user;
8788
}
8889

8990
/**

Controller/GraphQL/MeController.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
namespace TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL;
3+
4+
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\UserInterface;
13+
use Symfony\Component\Security\Core\User\UserProviderInterface;
14+
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
15+
use TheCodingMachine\GraphQLite\Annotations\Mutation;
16+
use TheCodingMachine\GraphQLite\Annotations\Query;
17+
use TheCodingMachine\Graphqlite\Bundle\Types\BasicUser;
18+
19+
class MeController
20+
{
21+
/**
22+
* @var TokenStorageInterface
23+
*/
24+
private $tokenStorage;
25+
26+
public function __construct(TokenStorageInterface $tokenStorage)
27+
{
28+
$this->tokenStorage = $tokenStorage;
29+
}
30+
31+
/**
32+
* @Query()
33+
*/
34+
public function me(): ?UserInterface
35+
{
36+
$token = $this->tokenStorage->getToken();
37+
if ($token === null) {
38+
return null;
39+
}
40+
41+
$user = $this->tokenStorage->getToken()->getUser();
42+
43+
if (!$user instanceof UserInterface) {
44+
// getUser() can be an object with a toString or a string
45+
$userName = (string) $user;
46+
$user = new BasicUser($userName);
47+
}
48+
49+
return $user;
50+
}
51+
}

DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public function getConfigTreeBuilder()
4040
->arrayNode('security')
4141
->children()
4242
->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+
->enumNode('enable_me')->values(['on', 'off', 'auto'])->defaultValue('auto')->info('Enable to automatically create a "me" query to fetch the current user. "on": enable, "auto": enable if security bundle is available.')->end()
4344
->scalarNode('firewall_name')->defaultValue('main')->info('The name of the firewall to use for login')->end()
4445
->end()
4546
->end()

DependencyInjection/GraphqliteCompilerPass.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
use TheCodingMachine\GraphQLite\Annotations\Parameter;
4848
use TheCodingMachine\GraphQLite\Annotations\Query;
4949
use TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController;
50+
use TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\MeController;
5051
use TheCodingMachine\GraphQLite\FieldsBuilder;
5152
use TheCodingMachine\GraphQLite\FieldsBuilderFactory;
5253
use TheCodingMachine\GraphQLite\GraphQLException;
@@ -115,7 +116,6 @@ public function process(ContainerBuilder $container)
115116
// If the security is disabled, let's remove the LoginController
116117
if ($disableLogin === true) {
117118
$container->removeDefinition(LoginController::class);
118-
$container->removeDefinition(AggregateControllerQueryProviderFactory::class);
119119
}
120120

121121
if ($container->getParameter('graphqlite.security.enable_login') === 'on') {
@@ -132,6 +132,36 @@ public function process(ContainerBuilder $container)
132132
$provider = $container->findDefinition('security.firewall.map.config.'.$firewallName)->getArgument(5);
133133

134134
$container->findDefinition(LoginController::class)->setArgument(0, new Reference($provider));
135+
136+
$this->registerController(LoginController::class, $container);
137+
}
138+
139+
$disableMe = false;
140+
if ($container->getParameter('graphqlite.security.enable_me') === 'auto'
141+
&& !$container->has(TokenStorageInterface::class)) {
142+
$disableMe = true;
143+
}
144+
if ($container->getParameter('graphqlite.security.enable_me') === 'off') {
145+
$disableMe = true;
146+
}
147+
// If the security is disabled, let's remove the LoginController
148+
if ($disableMe === true) {
149+
$container->removeDefinition(MeController::class);
150+
}
151+
152+
if ($container->getParameter('graphqlite.security.enable_me') === 'on') {
153+
if (!$container->has(TokenStorageInterface::class)) {
154+
throw new GraphQLException('In order to enable the "me" query (via the graphqlite.security.enable_me parameter), you need to install the security bundle.');
155+
}
156+
}
157+
158+
if ($disableMe === false) {
159+
$this->registerController(MeController::class, $container);
160+
}
161+
162+
// Perf improvement: let's remove the AggregateControllerQueryProviderFactory if it is empty.
163+
if (empty($container->findDefinition(AggregateControllerQueryProviderFactory::class)->getArgument(0))) {
164+
$container->removeDefinition(AggregateControllerQueryProviderFactory::class);
135165
}
136166

137167

@@ -220,6 +250,14 @@ public function process(ContainerBuilder $container)
220250
$this->mapAdderToTag('graphql.type_mapper_factory', 'addTypeMapperFactory', $container, $schemaFactory);
221251
}
222252

253+
private function registerController(string $controllerClassName, ContainerBuilder $container): void
254+
{
255+
$aggregateQueryProvider = $container->findDefinition(AggregateControllerQueryProviderFactory::class);
256+
$controllersList = $aggregateQueryProvider->getArgument(0);
257+
$controllersList[] = $controllerClassName;
258+
$aggregateQueryProvider->setArgument(0, $controllersList);
259+
}
260+
223261
/**
224262
* Register a method call on SchemaFactory for each tagged service, passing the service in parameter.
225263
*

DependencyInjection/GraphqliteExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,12 @@ public function load(array $configs, ContainerBuilder $container)
5555
}
5656

5757
$enableLogin = $configs[0]['security']['enable_login'] ?? 'auto';
58+
$enableMe = $configs[0]['security']['enable_me'] ?? 'auto';
5859

5960
$container->setParameter('graphqlite.namespace.controllers', $namespaceController);
6061
$container->setParameter('graphqlite.namespace.types', $namespaceType);
6162
$container->setParameter('graphqlite.security.enable_login', $enableLogin);
63+
$container->setParameter('graphqlite.security.enable_me', $enableMe);
6264
$container->setParameter('graphqlite.security.firewall_name', $configs[0]['security']['firewall_name'] ?? 'main');
6365

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

Resources/config/container/graphqlite.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
<call method="setAuthorizationService">
2121
<argument type="service" id="TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface" />
2222
</call>
23+
<!-- Ideally, we would not need to go through the GlobTypeMapper for types in packages. We shoul
24+
have the opportunity to add types / models through another class (like the AggregateControllerQueryProviderFactory for queries) -->
25+
<call method="addTypeNamespace">
26+
<argument type="string">TheCodingMachine\Graphqlite\Bundle\Types</argument>
27+
</call>
2328
</service>
2429

2530
<service id="TheCodingMachine\GraphQLite\Schema" public="true">
@@ -30,7 +35,6 @@
3035

3136
<service id="TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory">
3237
<argument type="collection">
33-
<argument>TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController</argument>
3438
</argument>
3539
<tag name="graphql.queryprovider_factory" />
3640
</service>
@@ -77,5 +81,9 @@
7781
<service id="TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController" public="true">
7882
<argument key="$firewallName">%graphqlite.security.firewall_name%</argument>
7983
</service>
84+
85+
<service id="TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\MeController" public="true" />
86+
87+
<service id="TheCodingMachine\Graphqlite\Bundle\Types\UserType" public="true"/>
8088
</services>
8189
</container>

Tests/FunctionalTest.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@ public function testLoginQuery(): void
220220

221221
$request = Request::create('/graphql', 'POST', ['query' => '
222222
mutation login {
223-
login(userName: "foo", password: "bar")
223+
login(userName: "foo", password: "bar") {
224+
userName
225+
}
224226
}']);
225227

226228
$response = $kernel->handle($request);
@@ -229,9 +231,34 @@ public function testLoginQuery(): void
229231

230232
$this->assertSame([
231233
'data' => [
232-
'login' => true
234+
'login' => [
235+
'userName' => 'foo'
236+
]
233237
]
234238
], $result);
239+
240+
$request = Request::create('/graphql', 'POST', ['query' => '
241+
{
242+
me {
243+
userName
244+
roles
245+
}
246+
}
247+
']);
248+
249+
$response = $kernel->handle($request);
250+
251+
$result = json_decode($response->getContent(), true);
252+
253+
$this->assertSame([
254+
'data' => [
255+
'me' => [
256+
'userName' => 'foo',
257+
'roles' => [],
258+
]
259+
]
260+
], $result);
261+
235262
}
236263

237264
public function testNoLoginNoSessionQuery(): void
@@ -241,7 +268,9 @@ public function testNoLoginNoSessionQuery(): void
241268

242269
$request = Request::create('/graphql', 'POST', ['query' => '
243270
mutation login {
244-
login(userName: "foo", password: "bar")
271+
login(userName: "foo", password: "bar") {
272+
userName
273+
}
245274
}']);
246275

247276
$response = $kernel->handle($request);

Types/BasicUser.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\Graphqlite\Bundle\Types;
5+
6+
use TheCodingMachine\GraphQLite\Annotations\Field;
7+
use TheCodingMachine\GraphQLite\Annotations\SourceField;
8+
use TheCodingMachine\GraphQLite\Annotations\Type;
9+
use Symfony\Component\Security\Core\User\UserInterface;
10+
11+
class BasicUser implements UserInterface
12+
{
13+
/**
14+
* @var string
15+
*/
16+
private $name;
17+
18+
public function __construct(string $name)
19+
{
20+
$this->name = $name;
21+
}
22+
23+
/**
24+
* Returns the roles granted to the user.
25+
*
26+
* public function getRoles()
27+
* {
28+
* return ['ROLE_USER'];
29+
* }
30+
*
31+
* Alternatively, the roles might be stored on a ``roles`` property,
32+
* and populated in any number of different ways when the user object
33+
* is created.
34+
*
35+
* @return (Role|string)[] The user roles
36+
*/
37+
public function getRoles()
38+
{
39+
return [];
40+
}
41+
42+
/**
43+
* Returns the password used to authenticate the user.
44+
*
45+
* This should be the encoded password. On authentication, a plain-text
46+
* password will be salted, encoded, and then compared to this value.
47+
*
48+
* @return string The password
49+
*/
50+
public function getPassword()
51+
{
52+
throw new \RuntimeException('getPassword is not implemented by BasicUser');
53+
}
54+
55+
/**
56+
* Returns the salt that was originally used to encode the password.
57+
*
58+
* This can return null if the password was not encoded using a salt.
59+
*
60+
* @return string|null The salt
61+
*/
62+
public function getSalt()
63+
{
64+
throw new \RuntimeException('getSalt is not implemented by BasicUser');
65+
}
66+
67+
/**
68+
* Returns the username used to authenticate the user.
69+
*
70+
* @return string The username
71+
*/
72+
public function getUsername()
73+
{
74+
return $this->name;
75+
}
76+
77+
/**
78+
* Removes sensitive data from the user.
79+
*
80+
* This is important if, at any given point, sensitive information like
81+
* the plain-text password is stored on this object.
82+
*/
83+
public function eraseCredentials()
84+
{
85+
throw new \RuntimeException('eraseCredentials is not implemented by BasicUser');
86+
}
87+
}

Types/UserType.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\Graphqlite\Bundle\Types;
5+
6+
use TheCodingMachine\GraphQLite\Annotations\Field;
7+
use TheCodingMachine\GraphQLite\Annotations\SourceField;
8+
use TheCodingMachine\GraphQLite\Annotations\Type;
9+
use Symfony\Component\Security\Core\User\UserInterface;
10+
11+
/**
12+
* @Type(class=UserInterface::class)
13+
* @SourceField(name="userName")
14+
*/
15+
class UserType
16+
{
17+
/**
18+
* @Field()
19+
* @return string[]
20+
*/
21+
public function getRoles(UserInterface $user): array
22+
{
23+
$roles = [];
24+
foreach ($user->getRoles() as $role) {
25+
$roles[] = (string) $role;
26+
}
27+
return $roles;
28+
}
29+
}

0 commit comments

Comments
 (0)