Describe the bug
When creating a mutation with both the @guard directive as well as the @validator directive on the input, the validation is processed before the guarding.
Expected behavior/Solution
I expect the guarding to be processed before the validation, because if someone is not authenticated, there's is no point in further handling the request with input validation.
Steps to reproduce
extend type Mutation @guard {
createStuff(input: StuffInput!): Stuff! @create
}
type Stuff @model(class: "App\\Models\\Stuff") {
name: String!
}
input StuffInput @validator {
name: String!
}
<?php
namespace App\GraphQL\Validators;
use Nuwave\Lighthouse\Validation\Validator;
class StuffInputValidator extends Validator
{
public function rules(): array
{
return [
'name' => [
'string',
'min:24',
],
];
}
}
<?php
namespace Tests;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\TestCase;
use Nuwave\Lighthouse\Testing\MakesGraphQLRequests;
class CreateStuffTest extends TestCase
{
use MakesGraphQLRequests;
public function createApplication(): Application
{
$app = require dirname(__DIR__) . DIRECTORY_SEPARATOR . 'bootstrap' . DIRECTORY_SEPARATOR . 'app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
/**
* @test
*/
public function it_throws_an_error_if_the_user_is_unauthenticated(): void
{
$response = $this->graphQL(/** @lang GraphQL */ '
mutation {
createStuff(input: {
name: "not-long-enough"
}) {
id
}
}
');
static::assertContains('Unauthenticated.', $response->json('errors.*.message'));
}
}
This test will fail, because the name attribute is not long enough. It should pass, because a user being unauthenticated should take precedence over validation.
Lighthouse Version
5.3.0
Hi I have the following case too:
extend type Mutation @guard(with: ["api"]) {
createPostulante(input: CreatePostulanteInput! @spread): Postulante @create
}
The input has a @validator directive and it always goes first even if I put it after @create. That seems wrong since I want to authenticate my user before validation is attempted.
@spawnia I've managed to put something together that does the trick, but I'm totally not sure if this the way to go. Just thinking out loud.
<?php
namespace Nuwave\Lighthouse\Auth;
use Closure;
use GraphQL\Language\AST\DirectiveNode;
use Nuwave\Lighthouse\Schema\Directives\GuardDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
class AuthDirective extends GuardDirective
{
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
if ($this->fieldHasGuardDirective($fieldValue)) {
return parent::handleField($fieldValue, $next);
}
return $next($fieldValue);
}
protected function fieldHasGuardDirective(FieldValue $fieldValue): bool
{
/** @var DirectiveNode $directive */
foreach ($fieldValue->getField()->directives as $directive) {
if ($directive->name->value === 'guard') {
$this->directiveNode = $directive;
return true;
}
}
return false;
}
}
'field_middleware' => [
\Nuwave\Lighthouse\Schema\Directives\TrimDirective::class,
\Nuwave\Lighthouse\Schema\Directives\SanitizeDirective::class,
\Nuwave\Lighthouse\Auth\AuthDirective::class, // <--- before validation
\Nuwave\Lighthouse\Validation\ValidateDirective::class,
\Nuwave\Lighthouse\Schema\Directives\TransformArgsDirective::class,
\Nuwave\Lighthouse\Schema\Directives\SpreadDirective::class,
\Nuwave\Lighthouse\Schema\Directives\RenameArgsDirective::class,
],
@wimski that would run @guard twice.
I think we should enhance the field middleware configuration to allow defining a default order for field middleware. That would allow interleaving optional field middleware directives such as @guard with global field middleware.