DEV Community

Cover image for [ PART 4 ] Creating a Twitter clone with GraphQL, Typescript, and React ( User Login )
ips-coding-challenge
ips-coding-challenge

Posted on • Updated on

[ PART 4 ] Creating a Twitter clone with GraphQL, Typescript, and React ( User Login )

Github Repo

Hi everyone. I hope I'm not tiring you out too much:D

Let's continue our project with the connection of a user.

src/resolvers/AuthResolver.ts

@Mutation(() => AuthResponse)
  async login(@Arg('input') input: LoginPayload, @Ctx() ctx: MyContext) {
    const { db } = ctx

    const [user] = await db('users').where('email', input.email)

    if (!user) {
      throw new ApolloError('Invalid credentials')
    }

    const isPasswordValid = await argon2.verify(user.password, input.password)

    if (!isPasswordValid) {
      throw new ApolloError('Invalid credentials')
    }

    const token = generateToken(user)

    return { token, user }
  }
Enter fullscreen mode Exit fullscreen mode

Nothing new here. I'm checking if I have a user corresponding to the email. Then I check if the passwords match and if everything is good, I generate a JWT token that I send back with the user.

As for the LoginPayload class which contains the validation rules, here it is:

src/dto/LoginPayload.ts

import { IsEmail } from 'class-validator'
import { Field, InputType } from 'type-graphql'

@InputType()
class LoginPayload {
  @Field()
  @IsEmail()
  email: string

  @Field()
  password: string
}

export default LoginPayload

Enter fullscreen mode Exit fullscreen mode

Here is the result in the GraphQL playground
Alt Text

Let's still write some tests ;)

src/tests/auth.test.ts

it('should log in a user', async () => {
  await createUser()
  const { mutate } = await testClient()

  const res = await mutate({
    mutation: LOGIN,
    variables: {
      input: {
        email: 'admin@test.fr',
        password: 'password',
      },
    },
  })

  const { token, user } = res.data.login
  expect(token).not.toBeNull()
  expect(user.username).toEqual('admin')
  expect(user.email).toEqual('admin@test.fr')
})

it('should throw a validation error if the email is invalid', async () => {
  await createUser()
  const { mutate } = await testClient()

  const res = await mutate({
    mutation: LOGIN,
    variables: {
      input: {
        email: 'adminaz',
        password: 'password',
      },
    },
  })

  expect(res.data).toBeNull()
  expect(res.errors).not.toBeNull()
  const {
    extensions: {
      exception: { validationErrors },
    },
  }: any = res.errors![0]

  expect((validationErrors[0] as ValidationError).constraints).toEqual({
    isEmail: 'email must be an email',
  })
})

it('should throw a validation error if the password is empty', async () => {
  await createUser()
  const { mutate } = await testClient()

  const res = await mutate({
    mutation: LOGIN,
    variables: {
      input: {
        email: 'admin@test.fr',
        password: '',
      },
    },
  })

  expect(res.data).toBeNull()
  expect(res.errors).not.toBeNull()
  const {
    extensions: {
      exception: { validationErrors },
    },
  }: any = res.errors![0]

  expect((validationErrors[0] as ValidationError).constraints).toEqual({
    isNotEmpty: 'password should not be empty',
  })
})
Enter fullscreen mode Exit fullscreen mode

Auth middleware

type-graphql has an authChecker option that can be passed to the buildSchema() method.

src/server.ts

import 'reflect-metadata'
import { ApolloServer } from 'apollo-server'
import { buildSchema } from 'type-graphql'
import AuthResolver from './resolvers/AuthResolver'
import db from './db/connection'
import { authChecker } from './middlewares/authChecker'

const createServer = async () => {
  return new ApolloServer({
    schema: await buildSchema({
      resolvers: [AuthResolver],
      authChecker: authChecker,
    }),
    context: ({ req, res }) => {
      return {
        req,
        res,
        db,
      }
    },
  })
}

export default createServer

Enter fullscreen mode Exit fullscreen mode

And it is in this function that we are going to check if we have an authenticated user.

src/middlewares/authChecker.ts

import { AuthChecker } from 'type-graphql'
import { MyContext } from '../types/types'
import { extractJwtToken } from '../utils/utils'
import jwt from 'jsonwebtoken'
import { JWT_SECRET } from '../config/config'

export const authChecker: AuthChecker<MyContext, string> = async ({
  root,
  args,
  context,
  info,
}) => {
  const { db, req } = <MyContext>context

  try {
    const token = extractJwtToken(req)
    const {
      data: { id },
    }: any = jwt.verify(token, JWT_SECRET as string)

    const [user] = await db('users').where('id', id)

    if (!user) throw new AuthenticationError('User not found')

    context.userId = user.id
    return true
  } catch (e) {
    throw e
  }
}

Enter fullscreen mode Exit fullscreen mode

The extractJwtToken() function just allows us to check that we have a header Authorization with a Bearer token. I let you check the Repository Github

To use this authChecker we just need to annotate the method with @Authorized.

src/resolvers/AuthResolver.ts

@Query(() => User)
@Authorized()
async me(@Ctx() ctx: MyContext) {
    const { db, userId } = ctx
    const [user] = await db('users').where('id', userId)

    return user
}
Enter fullscreen mode Exit fullscreen mode

If I try now without setting the "Authorization" header
Alt Text

And with the JWT Token in the Authorization header
Alt Text

Everything works as expected ;)

Ciao and take care! See you in the next part ^^

Top comments (0)