[ PART 3 ] Creating a Twitter clone with GraphQL, Typescript and React ( User Registration )
Tags: showdev, career, javascript, node
Published: 2021-01-06 16:45:14

[Github repo](https://github.com/ips-coding-challenge/challenge_twitter)

Hi everyone! Let's continue the project ;). I have to say that learning a new tech while writing those articles is harder than I first thought and takes a lot more time :D. 

## Register a user

Before starting, I add 2 fields that I forgot last time in the *users* table ;).

*src/db/migrations/add_fields_to_users_table.ts*

```ts
import * as Knex from 'knex'

export async function up(knex: Knex): Promise<void> {
  return knex.schema.alterTable('users', (t) => {
    t.string('display_name').notNullable()
    t.string('avatar')
  })
}

export async function down(knex: Knex): Promise<void> {
  return knex.schema.alterTable('users', (t) => {
    t.dropColumn('display_name')
    t.dropColumn('avatar')
  })
}

```

The **username** will be used as "slug" hence the fact that it is unique and not the **display_name**.

*src/entities/User.ts*

```typescript
@Field()
display_name: string

@Field()
avatar?: string
```

I will use a Token JWT based authorization. When a user logs in or registers, I will generate a JWT Token that I will send to the client. This token will then be passed to each request via an **Authorization** header and can then be checked to retrieve the logged-in user.

Let's install two new libraries ;)

```sh
yarn add jsonwebtoken argon2
```

Ok let's go to the AuthResolver to create our register mutation

*src/resolvers/AuthResolver.ts*

```typescript
@Mutation(() => AuthResponse)
  async register(@Arg('input') input: RegisterPayload, @Ctx() ctx: MyContext) {
    const { db } = ctx

    const hash = await argon2.hash(input.password)

    const [user] = await db('users')
      .insert({
        ...input,
        password: hash,
      })
      .returning('*')

    const token = generateToken(user)

    return { token, user }
  }
```

As you can see, I also created a **AuthResponse** and **RegisterPayload** class.

```typescript
@ObjectType()
class AuthResponse {
  @Field()
  token: string

  @Field(() => User)
  user: User
}
```

And it is in the **RegisterPayload** class that I will put the validation rules (via the **class-validator** library).

*src/dto/RegisterPayload.ts*

```typescript
import { IsEmail, Matches, MinLength } from 'class-validator'
import { Field, InputType } from 'type-graphql'

@InputType()
class RegisterPayload {
  @Field()
  @Matches(/^[a-zA-Z0-9_]{2,30}$/, {
    message:
      'The username should only contains alphanumeric characters and should have a length between 2 to 30',
  })
  username: string

  @Field()
  @MinLength(2)
  display_name: string

  @Field()
  @IsEmail()
  email: string

  @Field()
  @MinLength(6)
  password: string
}

export default RegisterPayload

```

Nothing special here. For the moment, the uniqueness of the email and username is only managed via the database. We'll see later how to create a custom Validation ;).


Otherwise if I go back to my **mutation**:

```typescript
@Mutation(() => AuthResponse)
  async register(@Arg('input') input: RegisterPayload, @Ctx() ctx: MyContext) {
    const { db } = ctx

    const hash = await argon2.hash(input.password)

    const [user] = await db('users')
      .insert({
        ...input,
        password: hash,
      })
      .returning('*')

    const token = generateToken(user)

    return { token, user }
  }
```

- I first get knex via context.
- I hash the password via the **argon2** library.
- I insert my user
- I generate a JWT token

As for the **generateToken** method, here it is

*src/utils/utils.ts*

```typescript
export const generateToken = (user: User) => {
  const token = jwt.sign(
    {
      data: {
        id: user.id,
        username: user.username,
        display_name: user.display_name,
      },
    },
    JWT_SECRET as string,
    { expiresIn: '7d' } // 7 days
  )
  return token
}
```

Note that the JWT_SECRET variable comes from a config file that I added to facilitate the use of environment variables.

*src/config/config.ts*

```typescript
import * as dotenv from 'dotenv'

dotenv.config({ path: `${__dirname}/../../.env.${process.env.NODE_ENV}` })

export const PORT = process.env.PORT
export const JWT_SECRET = process.env.JWT_SECRET

```

If I test my request with the GraphQL playground, I get this
![Register mutation](https://dev-to-uploads.s3.amazonaws.com/i/hfwx5wjan3y0aogu7e75.png)

I also wrote some tests
```typescript
import { gql } from 'apollo-server'
import knex from '../db/connection'
import { testClient } from './setup'
import { createUser } from './helpers'

const REGISTER = gql`
  mutation($input: RegisterPayload!) {
    register(input: $input) {
      token
      user {
        id
        username
        display_name
        email
        created_at
        updated_at
      }
    }
  }
`
beforeEach(async () => {
  await knex.migrate.rollback()
  await knex.migrate.latest()
})

afterEach(async () => {
  await knex.migrate.rollback()
})

test('it should register a user', async () => {
  const { mutate } = await testClient()

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

  const { token, user } = res.data.register
  expect(token).not.toBeNull()
  expect(user.username).toEqual('admin')
})
```

I'm only putting you on one test, but I've written others. You can see all this in the [Repo Github](https://github.com/ips-coding-challenge/challenge_twitter).

## Custom Unique Validation

At the moment we can't insert a user if the username or email already exists in the database but it's only managed by the database and we end up with an error that doesn't have the same format as the other validation errors. Let's fix this :D

*src/validators/Unique.ts*

```typescript
import {
  registerDecorator,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
} from 'class-validator'

import db from '../db/connection'

@ValidatorConstraint({ async: true })
export class UniqueConstraint implements ValidatorConstraintInterface {
  async validate(value: any, args: ValidationArguments) {
    const table = args.constraints[0]

    if (!table) throw new Error('Table argument is missing')

    const [item] = await db(table).where(args.property, value)
    if (!item) return true
    return false
  }
}

export function Unique(table: string, validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [table],
      validator: UniqueConstraint,
    })
  }
}

```

I strictly followed the examples to create this validation constraint. I just added the possibility to enter the table in which to make the SQL query in order to make the constraint more generic.

Otherwise, it's quite simple to understand. The **validate()** method retrieves the name of the table and will search if the property already exists in the database. For this to work, the name of the property must obviously match the name of the column in the table ;).

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ex7k3ci24mlltakoiqxk.png)

The error is now formatted like other validation errors.

Here is the test to verify that a user cannot register if his email is already taken

*src/tests/auth.test.ts*

```typescript
test('it should not register a user if the email already exists', async () => {
  await createUser('admin', 'admin@test.fr')

  const { mutate } = await testClient()

  const res = await mutate({
    mutation: REGISTER,
    variables: {
      input: {
        username: 'new',
        display_name: 'Admin',
        email: 'admin@test.fr',
        password: 'password',
      },
    },
  })

  expect(res.errors).not.toBeNull()

  const {
    extensions: {
      exception: { validationErrors },
    },
  }: any = res.errors![0]

  expect((validationErrors[0] as ValidationError).constraints).toEqual({
    UniqueConstraint: 'This email is already taken',
  })
  expect(res.data).toBeNull()
})
```

I'm not a fan of the validation errors format. I haven't yet looked at how I can intercept errors to format them in a simpler way. TypeGraphQL allows us to use middlewares but I don't know if we can use them globally. If we have to pass the middleware for each mutation to validate, it's not going to be great :D.


I think the **Register** part is coming to an end ;). In the next part, we'll see how to connect a user.

Ciao and have a nice day or evening ;)