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 }
}
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
Here is the result in the GraphQL playground
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',
})
})
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
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
}
}
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
}
If I try now without setting the "Authorization" header
And with the JWT Token in the Authorization header
Everything works as expected ;)
Ciao and take care! See you in the next part ^^
Top comments (0)