Skip to content

Commit

Permalink
feat: login
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
matheuspiment committed Oct 8, 2019
1 parent 645eba3 commit 414017c
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 1 deletion.
17 changes: 17 additions & 0 deletions __tests__/helpers/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Response } from 'express'

function createResponseObject (): Response {
return {
status: function (code) {
this.statusCode = code
return this
},
json: function () {
return {
status: this.statusCode
}
}
} as Response
}

export default createResponseObject
84 changes: 84 additions & 0 deletions __tests__/integration/controllers/AuthController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import request from 'supertest'

import app from '../../../src/app'
import MongooseConnection from '../../helpers/MongooseConnection'
import factory from '../../factories'

describe('AuthController', () => {
beforeAll(async () => {
await MongooseConnection.connect('AuthController')
})

afterEach(async () => {
await MongooseConnection.truncate()
})

afterAll(async () => {
await MongooseConnection.disconnect()
})

describe('login', () => {
it('should authenticate with valid credentials', async () => {
const user = await factory.attrs('User')

await request(app)
.post('/register')
.send(user)

const response = await request(app)
.post('/login')
.send({
email: user.email,
password: user.password
})

expect(response.body).toHaveProperty('token')
})
})

it('should not authenticate with invalid credentials', async () => {
const user = (await factory.create('User', { password: '123456' })).toObject()

const response = await request(app)
.post('/login')
.send({
email: user.email,
password: '123'
})

expect(response.status).toBe(401)
})

it('should not authenticate with nonexistent user', async () => {
const user = await factory.attrs('User', { email: '[email protected]' })

const response = await request(app)
.post('/login')
.send({
email: '[email protected]',
password: user.password
})

expect(response.status).toBe(401)
})

it('should not be able to authenticate with invalid args', async () => {
const user = await factory.attrs('User')

const responseWithoutEmail = await request(app)
.post('/login')
.send({
password: user.password
})

expect(responseWithoutEmail.status).toBe(400)

const responseWithoutPassword = await request(app)
.post('/login')
.send({
email: user.email
})

expect(responseWithoutPassword.status).toBe(400)
})
})
55 changes: 55 additions & 0 deletions __tests__/unit/middlewares/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import jwt from 'jsonwebtoken'

import { AuthRequest } from '../../../src/types'
import createResponseObject from '../../helpers/response'
import authMiddleware from '../../../src/middlewares/auth'
import authConfig from '../../../src/config/auth'

describe('authMiddleware', () => {
it('should append the userId property in the req object when a valid header is provided', async () => {
const token = jwt.sign({ _id: 'qwert' }, authConfig.secret, {
expiresIn: authConfig.expiresIn
})

const req = {
headers: {
authorization: `Bearer ${token}`
},
userId: ''
} as AuthRequest

const response = createResponseObject()

await authMiddleware(req, response, () => {})

expect(req.userId).toBe('qwert')
})

it('should return an error when a token is not provided', async () => {
const req = {
headers: {},
userId: ''
} as AuthRequest

const response = createResponseObject()

await authMiddleware(req, response, () => {})

expect(response.statusCode).toBe(401)
})

it('should return an error when the token provided is invalid', async () => {
const req = {
headers: {
authorization: 'Bearer qwert'
},
userId: ''
} as AuthRequest

const response = createResponseObject()

await authMiddleware(req, response, () => {})

expect(response.statusCode).toBe(401)
})
})
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "An awesome post API",
"main": "index.js",
"repository": "https://github.com/matheuspiment/alumnet-developer.git",
"repository": "https://github.com/matheuspiment/post-api.git",
"author": "Matheus Ribeiro Pimenta Nunes <[email protected]>",
"license": "MIT",
"private": true,
Expand All @@ -28,6 +28,7 @@
"cors": "^2.8.5",
"dotenv": "^8.1.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.15",
"mongoose": "^5.7.3",
"yup": "^0.27.0"
Expand All @@ -38,6 +39,7 @@
"@types/cors": "^2.8.6",
"@types/express": "^4.17.1",
"@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.3.4",
"@types/mongoose": "^5.5.19",
"@types/supertest": "^2.0.8",
"@typescript-eslint/eslint-plugin": "^2.3.2",
Expand Down
6 changes: 6 additions & 0 deletions src/config/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import '../bootstrap'

export default {
secret: process.env.JWT_SECRET,
expiresIn: '7d'
}
51 changes: 51 additions & 0 deletions src/controllers/AuthController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Request, Response } from 'express'
import * as Yup from 'yup'
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'

import User from '../schemas/User'
import authConfig from '../config/auth'

class AuthController {
async login (req: Request, res: Response): Promise<Response> {
const schema = Yup.object().shape({
email: Yup.string().email().required(),
password: Yup.string().required()
})

if (!(await schema.isValid(req.body))) {
return res.status(400).json({ error: 'Validation fails' })
}

try {
const { email, password } = req.body

const user = await User.findOne({ email })

if (!user) {
return res.status(401).send({ error: 'User not found' })
}

if (!(await bcrypt.compare(password, user.password))) {
return res.status(401).send({ error: 'Password does not match' })
}

const { _id, name } = user

return res.json({
user: {
_id,
name,
email
},
token: jwt.sign({ _id }, authConfig.secret, {
expiresIn: authConfig.expiresIn
})
})
} catch (err) {
return res.status(400).send({ error: 'Login failed' })
}
}
}

export default new AuthController()
26 changes: 26 additions & 0 deletions src/middlewares/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Response, NextFunction } from 'express'
import { promisify } from 'util'
import jwt from 'jsonwebtoken'

import { AuthRequest } from '../types'
import authConfig from '../config/auth'

export default async (req: AuthRequest, res: Response, next: NextFunction): Promise<Response | void> => {
const authHeader = req.headers.authorization

if (!authHeader) {
return res.status(401).json({ error: 'Token not provided' })
}

const [, token] = authHeader.split(' ')

try {
const decoded = await promisify(jwt.verify)(token, authConfig.secret) as unknown as { _id: string }

req.userId = decoded._id

return next()
} catch (error) {
return res.status(401).json({ error: 'Token invalid' })
}
}
15 changes: 15 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { Router } from 'express'

import authMiddleware from './middlewares/auth'

import UserController from './controllers/UserController'
import AuthController from './controllers/AuthController'

const routes = Router()

// Auth
routes.post('/login', AuthController.login)

// User
routes.post('/register', UserController.register)

// Auth Middleware
routes.use(authMiddleware)

// Post
routes.post('/create/post', (req, res) => {
return res.json({ post: 'Oi' })
})

export default routes
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Request } from 'express'

export interface AuthRequest extends Request {
userId: string
}
Loading

0 comments on commit 414017c

Please sign in to comment.