Skip to content

Commit ffb0685

Browse files
authored
feat(data producer): Add password reset mutation data producer with violations (#1013)
1 parent 45c6f1d commit ffb0685

File tree

3 files changed

+230
-0
lines changed

3 files changed

+230
-0
lines changed

src/GraphQL/Response/Response.php

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Drupal\graphql\GraphQL\Response;
6+
7+
use Drupal\jobiqo_graphql\Wrappers\Violation\ViolationCollection;
8+
9+
/**
10+
* Base class for responses containing the violations.
11+
*/
12+
class Response implements ResponseInterface {
13+
14+
/**
15+
* List of violations.
16+
*
17+
* @var array
18+
*/
19+
protected $violations = [];
20+
21+
/**
22+
* {@inheritdoc}
23+
*/
24+
public function addViolation($message, array $properties = []): void {
25+
$properties['message'] = (string) $message;
26+
$this->violations[] = $properties;
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function addViolations(array $messages, array $properties = []): void {
33+
foreach ($messages as $message) {
34+
$this->addViolation($message, $properties);
35+
}
36+
}
37+
38+
/**
39+
* {@inheritdoc}
40+
*/
41+
public function getViolations(): array {
42+
return $this->violations;
43+
}
44+
45+
}
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Drupal\graphql\GraphQL\Response;
6+
7+
/**
8+
* Response interface used for GraphQL responses.
9+
*/
10+
interface ResponseInterface {
11+
12+
/**
13+
* Adds the violation.
14+
*
15+
* @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
16+
* Violation message.
17+
* @param array $properties
18+
* Other properties related to the violation.
19+
*/
20+
public function addViolation($message, array $properties = []): void;
21+
22+
/**
23+
* Adds multiple violations.
24+
*
25+
* @param string[]|\Drupal\Core\StringTranslation\TranslatableMarkup[] $messages
26+
* Violation messages.
27+
* @param array $properties
28+
* Other properties related to the violation.
29+
*/
30+
public function addViolations(array $messages, array $properties = []): void;
31+
32+
/**
33+
* Gets the violations.
34+
*
35+
* @return array
36+
* Violations.
37+
*/
38+
public function getViolations(): array;
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace Drupal\graphql\Plugin\GraphQL\DataProducer\User;
4+
5+
use Drupal\Core\Logger\LoggerChannelInterface;
6+
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
7+
use Drupal\Core\StringTranslation\StringTranslationTrait;
8+
use Drupal\graphql\GraphQL\Response\Response;
9+
use Drupal\graphql\GraphQL\Response\ResponseInterface;
10+
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
11+
use Drupal\user\Controller\UserAuthenticationController;
12+
use Symfony\Component\DependencyInjection\ContainerInterface;
13+
use Symfony\Component\HttpFoundation\Request;
14+
15+
/**
16+
* Resets the user's password (mutation).
17+
*
18+
* @DataProducer(
19+
* id = "password_reset",
20+
* name = @Translation("Password reset"),
21+
* description = @Translation("Allows to reset the password."),
22+
* consumes = {
23+
* "email" = @ContextDefinition("email",
24+
* label = @Translation("Email")
25+
* )
26+
* }
27+
* )
28+
*/
29+
class PasswordReset extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
30+
31+
use StringTranslationTrait;
32+
33+
/**
34+
* The current request.
35+
*
36+
* @var \Symfony\Component\HttpFoundation\Request
37+
*/
38+
protected $currentRequest;
39+
40+
/**
41+
* The logger service.
42+
*
43+
* @var \Drupal\Core\Logger\LoggerChannelInterface
44+
*/
45+
protected $logger;
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
51+
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
52+
$request_stack = $container->get('request_stack');
53+
/** @var \Drupal\Core\Logger\LoggerChannelInterface $logger */
54+
$logger = $container->get('logger.channel.graphql');
55+
return new static(
56+
$configuration,
57+
$plugin_id,
58+
$plugin_definition,
59+
$request_stack->getCurrentRequest(),
60+
$logger
61+
);
62+
}
63+
64+
/**
65+
* UserRegister constructor.
66+
*
67+
* @param array $configuration
68+
* A configuration array containing information about the plugin instance.
69+
* @param string $plugin_id
70+
* The plugin_id for the plugin instance.
71+
* @param array $plugin_definition
72+
* The plugin implementation definition.
73+
* @param \Symfony\Component\HttpFoundation\Request $current_request
74+
* The current request.
75+
* @param \Drupal\Core\Logger\LoggerChannelInterface $logger
76+
* The logger service.
77+
*/
78+
public function __construct(
79+
array $configuration,
80+
string $plugin_id,
81+
array $plugin_definition,
82+
Request $current_request,
83+
LoggerChannelInterface $logger
84+
) {
85+
parent::__construct($configuration, $plugin_id, $plugin_definition);
86+
$this->currentRequest = $current_request;
87+
$this->logger = $logger;
88+
}
89+
90+
/**
91+
* Creates an user.
92+
*
93+
* @param string $email
94+
* The email address to reset the password for.
95+
*
96+
* @return \Drupal\graphql\GraphQL\Response\ResponseInterface
97+
* Response for password reset mutation with violations in case of failure.
98+
*/
99+
public function resolve(string $email): ResponseInterface {
100+
$content = [
101+
'mail' => $email,
102+
];
103+
104+
// Drupal does not have a user authentication service so we need to use the
105+
// authentication controller instead.
106+
$controller = UserAuthenticationController::create(\Drupal::getContainer());
107+
// Build up an authentication request for controller out of current request
108+
// but replace the request body with proper content. This way most of the
109+
// data are reused including the client's IP which is needed for flood
110+
// control. The request body is the only thing (besides client's IP) which
111+
// is pulled from the request within controller.
112+
$auth_request = new Request(
113+
$this->currentRequest->query->all(),
114+
$this->currentRequest->request->all(),
115+
$this->currentRequest->attributes->all(),
116+
$this->currentRequest->cookies->all(),
117+
$this->currentRequest->files->all(),
118+
$this->currentRequest->server->all(),
119+
json_encode($content)
120+
);
121+
$auth_request->setRequestFormat('json');
122+
123+
$response = new Response();
124+
try {
125+
$controller_response = $controller->resetPassword($auth_request);
126+
}
127+
catch (\Exception $e) {
128+
// Show general error message so potential attacker cannot abuse endpoint
129+
// to eg check if some email exist or not. Log to watchdog for potential
130+
// further investigation.
131+
$this->logger->warning($e->getMessage());
132+
$response->addViolation($this->t('Unable to reset password, please try again later.'));
133+
return $response;
134+
}
135+
// Show general error message also in case of unexpected response. Log to
136+
// watchdog for potential further investigation.
137+
if ($controller_response->getStatusCode() !== 200) {
138+
$this->logger->warning("Unexpected response code @code during password reset.", ['@code' => $response->getStatusCode()]);
139+
$response->addViolation($this->t('Unable to reset password, please try again later.'));
140+
}
141+
142+
return $response;
143+
}
144+
145+
}

0 commit comments

Comments
 (0)