Skip to content

Commit

Permalink
feat(auth-login): implement user login functionality
Browse files Browse the repository at this point in the history
- create POST /auth/login endpoint
- install and use jsonwebtoken
- write tests for POST /auth/login
- add JWT_SECRET to environmental variables

[Finishes #160819929]
  • Loading branch information
akhilome committed Oct 1, 2018
1 parent 6624a4a commit e265671
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 2 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PORT=[server_port_number]
DATABASE_URL=postgres://[username]@[server]:[port]/[database_name]
TEST_DATABASE_URL=postgres://[username]@[server]:[port]/[database_name]
JWT_SECRET=[your_supersecret_secret]
90 changes: 90 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"body-parser": "^1.18.3",
"dotenv": "^6.0.0",
"express": "^4.16.3",
"jsonwebtoken": "^8.3.0",
"pg": "^7.4.3"
},
"devDependencies": {
Expand Down
34 changes: 34 additions & 0 deletions server/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,40 @@ class AuthController {
return res.status(400).json({ error });
}
}

static async signin(req, res, next) {
const { email, password } = req;

try {
// Check if a user with the provided email exists
const userExists = (await pool.query('SELECT * FROM users WHERE email=$1', [email])).rowCount;
if (!userExists) {
return res.status(400).json({
status: 'error',
message: 'no user with that email exists',
});
}

const userDetails = (await pool.query('SELECT * FROM users WHERE email=$1', [email])).rows[0];
const correctPassword = await bcrpyt.compare(password, userDetails.password);

if (!correctPassword) {
return res.status(400).json({
status: 'error',
message: 'incorrect password',
});
}

// Append important payload to request object
req.userId = userDetails.id;
req.userName = userDetails.name;
req.userEmail = userDetails.email;
req.userStatus = userDetails.is_admin ? 'admin' : 'customer';
return next();
} catch (error) {
return res.status(400).json({ error });
}
}
}

export default AuthController;
30 changes: 30 additions & 0 deletions server/middleware/authHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';

dotenv.config();

class AuthHandler {
static async generateAuthToken(req, res) {
const {
userId,
userName,
userEmail,
userStatus,
} = req;

const token = jwt.sign({
userId,
userName,
userEmail,
userStatus,
}, process.env.JWT_SECRET);

res.status(200).json({
status: 'success',
message: 'user logged in successfully',
auth_token: token,
});
}
}

export default AuthHandler;
22 changes: 22 additions & 0 deletions server/middleware/sanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ class Sanitize {
req.password = password.trim();
return next();
}

static signin(req, res, next) {
const { email, password } = req.body;

if (!email || !password) {
return res.status(400).json({
status: 'error',
message: 'some required fields missing',
});
}

if (!Validator.isEmail(email) || !Validator.isValidPassword(password)) {
return res.status(400).json({
status: 'error',
message: 'email or password not correctly formatted',
});
}

req.email = email.trim();
req.password = password.trim();
return next();
}
}

export default Sanitize;
3 changes: 3 additions & 0 deletions server/routes/authRouter.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Router } from 'express';
import AuthController from '../controllers/authController';
import Sanitize from '../middleware/sanitizer';
import AuthHandler from '../middleware/authHandler';

const router = new Router();

router.post('/signup', Sanitize.signup, AuthController.signup);
router.post('/login', Sanitize.signin, AuthController.signin, AuthHandler.generateAuthToken);

export default router;
58 changes: 57 additions & 1 deletion tests/routes/auth.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'chai/register-should';
import chaiHttp from 'chai-http';
import dirtyChai from 'dirty-chai';
import app from '../../server/index';
import { seedData, populateTables } from '../seed/seed';
import { seedData, populateTables, populateUsersTable } from '../seed/seed';

chai.use(chaiHttp);
chai.use(dirtyChai);
Expand Down Expand Up @@ -98,3 +98,59 @@ describe('POST /auth/signup', () => {
});
});
});

describe('POST /auth/login', () => {
beforeEach(populateTables);
beforeEach(populateUsersTable);

it('should sign an existing user in', (done) => {
chai.request(app)
.post('/api/v1/auth/login')
.send(seedData.users.validUser)
.end((err, res) => {
if (err) done(err);

res.status.should.eql(200);
res.body.should.be.an('object').which.has.keys(['status', 'message', 'auth_token']);
done();
});
});

it('should respond with a 400 if required fields are missing', (done) => {
chai.request(app)
.post('/api/v1/auth/login')
.send(seedData.users.invalidUserNoData)
.end((err, res) => {
if (err) done(err);

res.status.should.eql(400);
done();
});
});

it('should not sign user in if invalid email or password is provided', (done) => {
chai.request(app)
.post('/api/v1/auth/login')
.send(seedData.users.invalidUser)
.end((err, res) => {
if (err) done(err);

res.status.should.eql(400);
res.body.should.not.have.keys(['auth_token']);
done();
});
});

it('should not sign in user with improper input format', (done) => {
chai.request(app)
.post('/api/v1/auth/login')
.send({ email: 'invalid@email', password: 'well?' })
.end((err, res) => {
if (err) done(err);

res.status.should.eql(400);
res.body.should.not.have.keys(['auth_token']);
done();
});
});
});
25 changes: 24 additions & 1 deletion tests/seed/seed.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import bcrypt from 'bcryptjs';
import pool from '../../server/db/config';

const seedData = {
Expand All @@ -14,6 +15,11 @@ const seedData = {
password: 'pixel2user',
confirmPassword: 'pixel2user',
},
invalidUser: {
name: 'four-O-four',
email: 'no@email.address',
password: 'invalid',
},
invalidUserNoData: {},
invalidUserNoName: {
email: 'unserious@lad.com',
Expand Down Expand Up @@ -53,4 +59,21 @@ const populateTables = async () => {
await pool.query(createUsersTableQuery);
};

export { seedData, populateTables };
const populateUsersTable = async () => {
// hash passwords
const adminHashedPassword = await bcrypt.hash(seedData.users.admin.password, 10);
const userHashedPassword = await bcrypt.hash(seedData.users.validUser.password, 10);
const insertQuery = 'INSERT INTO users(name, email, password, is_admin) VALUES($1, $2, $3, $4)';
// Admin user
await pool.query(
insertQuery,
[seedData.users.admin.name, seedData.users.admin.email, adminHashedPassword, 't'],
);
// Customer
await pool.query(
insertQuery,
[seedData.users.validUser.name, seedData.users.validUser.email, userHashedPassword, 'f'],
);
};

export { seedData, populateTables, populateUsersTable };

0 comments on commit e265671

Please sign in to comment.