Skip to content

Commit

Permalink
Merge pull request #34 from andela/ft-user-login-164395525
Browse files Browse the repository at this point in the history
#164395525: user should be able to login
  • Loading branch information
Dsalz committed Mar 6, 2019
2 parents 53da505 + 57ac044 commit 1cbeeb0
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 3 deletions.
4 changes: 4 additions & 0 deletions server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import passport from 'passport';
import dotenv from 'dotenv';
import expressSession from 'express-session';
import routes from './routes';
import Trimmer from './middlewares/Trimmer';

dotenv.config();

Expand All @@ -25,6 +26,9 @@ app.use(bodyParser.json());
// Initializing Passport
app.use(passport.initialize());

// Trimming data
app.use(Trimmer.trimBody);

// Creating user session
app.use(expressSession({
secret: process.env.SESSION_SECRET,
Expand Down
27 changes: 27 additions & 0 deletions server/controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,31 @@ export default class Users {
response(res).serverError({ message: err });
}
}

/**
* @description contoller function that logs a user in
* @param {object} req - request object
* @param {object} res - response object
* @returns {object} user - Logged in user
*/
static async loginUser(req, res) {
const { email, username, bio, image } = req.user.dataValues;
try {
const userToken = await HelperUtils.generateToken(req.user.dataValues);
response(res).success({
message: 'user logged in successfully',
user: {
email,
username,
bio,
image,
token: userToken
}
});
} catch (error) {
response(res).serverError({
message: 'Could not generate token'
});
}
}
}
40 changes: 40 additions & 0 deletions server/middlewares/Trimmer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @class Trimmer
* @description Trims data
* @exports AuthenticateUser
*/
class Trimmer {
/**
* @method trimValues
* @description Takes in an object and returns it with all the values trimmed
* @param {object} data - The object to be trimmed
* @returns {object} - trimmed data
*/
static trimValues(data) {
const dataKeys = Object.keys(data);
const trimmedData = {};
for (let i = 0; i < dataKeys.length; i += 1) {
const key = dataKeys[i];
const value = data[key];
trimmedData[key] = (key !== 'password' && typeof value === 'string') ? value.trim() : value;
}
return trimmedData;
}

/**
* @method trimBody
* @description Trims the request body
* @param {object} req - The Request Object
* @param {object} res - The Response Object
* @param {callback} next - Callback method
* @returns {undefined} -
*/
static trimBody(req, res, next) {
if (req.body) {
req.body = Trimmer.trimValues(req.body);
}
return next();
}
}

export default Trimmer;
67 changes: 66 additions & 1 deletion server/middlewares/ValidateUser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { check, validationResult } from 'express-validator/check';
import Sequelize from 'sequelize';
import bcrypt from 'bcryptjs';
import models from '../database/models';
import { response } from '../utils';
import { response, validationErrors } from '../utils';

const { Op } = Sequelize;
const { User } = models;
Expand Down Expand Up @@ -83,6 +84,70 @@ class ValidateUser {
const existingUserObject = await user;
return user.length > 0 ? Object.keys(existingUserObject[0]) : [];
}

/**
* @method validateLoginFields
* @description Validates details provided by user when logging in
* @returns {array} - Array of validation methods
*/
static validateLoginFields() {
return [
check('name')
.exists({
checkNull: true,
checkFalsy: true,
})
.withMessage('Email or Username is required to login'),

check('password')
.exists({
checkNull: true,
checkFalsy: true,
})
.withMessage('Password is required to login')
];
}

/**
* @method validateLogin
* @description Validates details provided by user when logging in
* @param {object} req - request object
* @param {object} res - response object
* @param {object} next - function to pass to next middleware
* @returns {undefined}
*/
static async validateLogin(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return response(res).badRequest({
errors: validationErrors(errors)
});
}
const { name, password } = req.body;
const user = await User.findOne({
where: {
[Op.or]: [{ email: name }, { username: name }]
}
});
if (!user) {
return response(res).notFound({
message: 'invalid credentials'
});
}
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return response(res).forbidden({
message: 'invalid credentials'
});
}
if (!user.verifiedEmail) {
return response(res).forbidden({
message: 'You have to verify your email before you login'
});
}
req.user = user;
next();
}
}

export default ValidateUser;
2 changes: 0 additions & 2 deletions server/middlewares/newMiddle.js

This file was deleted.

4 changes: 4 additions & 0 deletions server/routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ authRoute.post('/users',
ValidateUser.validateMethods(),
ValidateUser.validateUserDetails,
Users.signupUser);
authRoute.post('/users/login',
ValidateUser.validateLoginFields(),
ValidateUser.validateLogin,
Users.loginUser);
authRoute.get('/users/verifyemail', Users.verifyUserEmail);
authRoute.post('/users/reset-password', Users.resetPasswordEmail);
authRoute.patch('/users/reset-password', Users.resetPassword);
Expand Down
152 changes: 152 additions & 0 deletions server/test/user.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const resetPasswordURL = `/api/users/reset-password${QueryURL}`;
const invalidResetPasswordURL = `/api/users/reset-password${invalidQueryURL}`;
const verifyURL = `/api/users/verifyemail${QueryURL}`;
const invalidVerifyURL = `/api/users/verifyemail${invalidQueryURL}`;
const secondTestEmail = 'thatsecondemail@yahoo.com';
const secondQueryURL = `?email=${secondTestEmail}&hash=${HelperUtils.hashPassword(secondTestEmail)}`;
const secondVerifyURL = `/api/users/verifyemail${secondQueryURL}`;
const thirdTestEmail = 'thatthirdemail@yahoo.com';
const loginURL = '/api/users/login';


describe('Test signup endpoint and email verification endpoint', () => {
it("It should return a 404 if user don't exist during email verification", (done) => {
Expand Down Expand Up @@ -287,3 +293,149 @@ describe('Social Login with Facebook', () => {
});
});
});

describe('Test login endpoint', () => {
before((done) => {
const secondData = {
firstname: 'John',
lastname: 'Doe',
email: secondTestEmail,
username: 'numerotwo222',
password: '22222222'
};
chai
.request(app)
.post(signupURL)
.send(secondData)
.end(() => {
chai
.request(app)
.get(secondVerifyURL)
.end(() => {
const thirdData = {
firstname: 'John',
lastname: 'Doe',
email: thirdTestEmail,
username: 'numerothree333',
password: '33333333'
};
chai
.request(app)
.post(signupURL)
.send(thirdData)
.end(() => {
done();
});
});
});
});
it('should log in a user who has verified his email when name is set to email', (done) => {
const data = {
name: secondTestEmail,
password: '22222222'
};
chai
.request(app)
.post(loginURL)
.send(data)
.end((err, res) => {
expect(res.status).to.equal(200);
const { message, user } = res.body;
expect(message).to.equal('user logged in successfully');
expect(user.email).to.equal(secondTestEmail);
expect(user.username).to.equal('numerotwo222');
done();
});
});
it('should log in a user who has verified his email when name is set to username', (done) => {
const data = {
name: 'numerotwo222',
password: '22222222'
};
chai
.request(app)
.post(loginURL)
.send(data)
.end((err, res) => {
expect(res.status).to.equal(200);
const { message, user } = res.body;
expect(message).to.equal('user logged in successfully');
expect(user.email).to.equal(secondTestEmail);
expect(user.username).to.equal('numerotwo222');
done();
});
});
it('should not log in a user who hasnt verified his email', (done) => {
const data = {
name: thirdTestEmail,
password: '33333333'
};
chai
.request(app)
.post(loginURL)
.send(data)
.end((err, res) => {
expect(res.status).to.equal(403);
expect(res.body.message).to.equal('You have to verify your email before you login');
done();
});
});
it('should not log in a user with wrong password', (done) => {
const data = {
name: secondTestEmail,
password: '77777777'
};
chai
.request(app)
.post(loginURL)
.send(data)
.end((err, res) => {
expect(res.status).to.equal(403);
expect(res.body.message).to.equal('invalid credentials');
done();
});
});
it('should not log in a user with wrong username and email', (done) => {
const data = {
name: `${secondTestEmail}djdhff`,
password: '22222222'
};
chai
.request(app)
.post(loginURL)
.send(data)
.end((err, res) => {
expect(res.status).to.equal(404);
expect(res.body.message).to.equal('invalid credentials');
done();
});
});
it('should not log in a user without password', (done) => {
const data = {
name: secondTestEmail,
};
chai
.request(app)
.post(loginURL)
.send(data)
.end((err, res) => {
expect(res.status).to.equal(400);
expect(res.body.errors.password[0]).to.equal('Password is required to login');
done();
});
});
it('should not log in a user without email or username', (done) => {
const data = {
password: '22222222'
};
chai
.request(app)
.post(loginURL)
.send(data)
.end((err, res) => {
expect(res.status).to.equal(400);
expect(res.body.errors.name[0]).to.equal('Email or Username is required to login');
done();
});
});
});
30 changes: 30 additions & 0 deletions swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,36 @@ paths:
description: email verified successfully
'400':
description: invalid email
/users/login:
post:
tags:
- user
summary: Logs a user in
description: This endpoint logs a user in using their password and email/username
consumes:
- application/json
produces:
- application/json
parameters:
- name: name
in: body
description: Email/username of the user
required: true
type: string
- name: password
in: body
description: User's password
required: true
type: string
responses:
'200':
description: user logged in successfully
'400':
description: Email or Username is required to login
'400':
description: Password is required to login
'403':
description: invalid credentials
/articles:
post:
tags:
Expand Down

0 comments on commit 1cbeeb0

Please sign in to comment.