Why is my ApolloGraphQL Mutation not returning an error message when the condition is met?

Viewed 29

This is my first question on here.

I have created login/register mutations that are working, However, I want to set up error handling so that when the user enters an incorrect username, it returns an error message "username incorrect", and the same with the password.

Currently, the error message returns when a user enters an incorrect username, but not when the user enters an incorrect password. Entering an incorrect password returns an object displaying a null user like so:

{
  "data": {
    "login": {
      "user": null
    }
  }
}

I also have a register mutation that is working, however, the errors are not returning when an error condition is met, again, it returns a null user.

I have pasted the code below, the login mutation is the last one at the bottom. The register mutation is above this.

Thank you

import { User } from '../entities/User';
import { MyContext } from 'src/types';
import argon2 from 'argon2';

import {
  Resolver,
  Mutation,
  Arg,
  InputType,
  Field,
  Ctx,
  ObjectType,
  Query,
} from 'type-graphql';

@InputType()
class UsernamePasswordInput {
  @Field()
  username: string;
  @Field()
  password: string;
}

@ObjectType()
class FieldError {
  @Field()
  field: string;
  message: string;
}

@ObjectType()
class UserResponse {
  @Field(() => [FieldError], { nullable: true })
  errors?: FieldError[];

  @Field(() => User, { nullable: true })
  user?: User;
}

// SAVE USER TO DATABASE
@Resolver()
export class UserResolver {
  // GET ALL USERS

  @Query(() => [User])
  getAllUsers(@Ctx() { em }: MyContext): Promise<User[]> {
    return em.find(User, {});
  }

  @Mutation(() => UserResponse)
  async register(
    @Arg('options') options: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<UserResponse> {
    // username
    if (options.username.length <= 2) {
      return {
        errors: [
          {
            field: 'username',
            message: 'length must be greater than 2',
          },
        ],
      };
    }
    // password
    if (options.password.length <= 3) {
      return {
        errors: [
          {
            field: 'password',
            message: 'length must be greater than 3',
          },
        ],
      };
    }
    // hash password
    const hashedPassword = await argon2.hash(options.password);
    const user = em.create(User, {
      username: options.username,
      password: hashedPassword,
    });
    await em.persistAndFlush(user);
    return {
      user,
    };
  }

  @Mutation(() => UserResponse)
  async login(
    @Arg('options') options: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<UserResponse> {
    const user = await em.findOneOrFail(User, {
      username: options.username,
    });

    if (!user) {
      return {
        errors: [
          {
            field: 'username',
            message: ' incorrect username',
          },
        ],
      };
    }
    // verify the user password
    const valid = await argon2.verify(user.password, options.password);

    // if password is not valid, return errors
    if (!valid) {
      return {
        errors: [
          {
            field: 'password',
            message: 'incorrect password',
          },
        ],
      };
    }
    // return user
    return {
      user,
    };
  }
}

1 Answers

Some notes:

  1. JavaScript has built-in error-handling support, use it.

  2. You must follow GraphQL Validation and Apollo Server Error handling guidelines.

  3. TypeGraphQL has built-in support for argument and input validation via Custom Scalars and class-validator.

With the above notes/assumptions, your code can be rewritten (and simplified) as follows:

UsernamePasswordInput:

// ...
import { Length } from 'class-validator'

@InputType()
class UsernamePasswordInput {
  @Field()
  @Length(3, 32) // length >= 3 and <= 32
  username: string;

  @Field()
  @Length(4, 16) // length >= 4 and <= 16
  password: string;
}

UserResolver:
Note that when checking username and password we leverage JavaScript Operator Precedence (Short-circuiting). Moreover, the error thrown is more generic, hiding possible "sensible/useful" information without showing if it's the username or the password that is invalid.

// ...
import { AuthenticationError } from 'apollo-server-errors'

@Resolver(() => User)
class UserResolver {
  // ...

  @Mutation(() => User)
  async register(
    @Arg('data') data: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<User> {
    // username and password length already checked

    // Hash password
    const hashedPassword = await argon2.hash(data.password);
    
    // Create user
    const user = em.create(User, {
      username: data.username,
      password: hashedPassword,
    });
    await em.persistAndFlush(user);

    // Return new user
    return user;
  }

  @Mutation(() => User)
  async login(
    @Arg('data') data: UsernamePasswordInput,
    @Ctx() { em }: MyContext
  ): Promise<User> {
    // Find user without DB error (hide information) 
    const user = await em.findOne(User, {
      username: data.username,
    });

    // Check username and password
    // Operator Precedence (Short-circuiting)
    // Generic authentication error
    if (!user ||
        !(await argon2.verify(user.password, data.password))
    ) throw new AuthenticationError("Invalid username or password");

    // ...

    // Return authenticated user
    return user;
  }
}
Related