Skip to content

Commit

Permalink
[Feat]: receive jwt on signup
Browse files Browse the repository at this point in the history
- Implements receipt of jwt token on signup
- Implements correct controller on signup/login page
- Implements caching of data in browser

[Finishes #158457035]
  • Loading branch information
Hasstrup committed Jul 7, 2018
1 parent 61eaa4e commit bc9f620
Show file tree
Hide file tree
Showing 23 changed files with 475 additions and 332 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
sudo: required
addons:
chrome: stable
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start

language: node_js

Expand Down
45 changes: 43 additions & 2 deletions app/controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ const signin = (req, res) => {
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @param {function} next - next function for passing the request to next handler
* @param {object} passport - passport with all the startegies registered
* @description Controller for handling requests to '/api/auth/login', returns token in response as JSON.
* Uses Tokenizer helper method to handle generation of token
*/
Expand All @@ -56,6 +55,48 @@ const handleLogin = (req, res, next) => {
})(req, res, next);
};

/**
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @param {function} next - next function for passing the request to next handler
* @description Controller for handling requests to '/api/auth/signup',
* returns the token of the user on signup, users Tokenizer to generate the token as well.
*/
const handleSignUp = (req, res, next) => {
// there has to be the email, username and password
if (req.body.password && req.body.email && req.body.name) {
// Check that there is no user with that email
User.findOne({ email: req.body.email }, (err, existingUser) => {
if (err) return next(err);
if (!existingUser) {
const user = new User(req.body);
user.avatar = avatarsArray[user.avatar];
user.provider = 'local';
user.save((err, newUser) => {
if (err) return next(err); // something went wrong saving the new user
const token = Tokenizer(newUser);
return res.status(201).json({ ...newUser._doc, token });
});
return;
}
// conflict errors
const error = new Error('Sorry that user exists already exists');
error.status = 409;
return next(error);
});
} else {
// Loop through to find the missing fields, so the error message is pretty clear
const required = ['password', 'name', 'email'];
const message = required.reduce((accumulator, current) => {
if (!req.body[`${current}`]) return `${accumulator}, ${current}`;
return accumulator;
}, 'Hey, Please check that these fields are present');
const error = new Error(`${message}.`);
error.status = 422;
return next(error);
}
};


/**
* @param {object} req - request object provided by express
Expand Down Expand Up @@ -185,7 +226,6 @@ const addDonation = (req, res) => {
}
}
if (!duplicate) {
console.log('Validated donation');
user.donations.push(req.body);
user.premium = 1;
user.save();
Expand Down Expand Up @@ -251,5 +291,6 @@ export default {
signup,
signin,
handleLogin,
handleSignUp,
avatars
};
6 changes: 3 additions & 3 deletions app/helpers/tokenizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import jwt from 'jsonwebtoken';
*/
const verifyPayload = (data) => {
// make sure the neccessary keys are present, only works if the data passed is an object :)
if (data.username && data._id && data.name) return;
if (data._id && data.name) return;
throw new TypeError("The object passed in should have keys 'id', 'username', 'name' and 'avatar'");
};

Expand All @@ -28,8 +28,8 @@ const verifyPayload = (data) => {
*/
export const Tokenizer = (payload) => {
verifyPayload(payload);
const { name, username, avatar, _id } = payload;
const filteredPayload = { name, username, avatar, _id }; /* eslint object-curly-newline: 0 */
const { name, avatar, _id } = payload;
const filteredPayload = { name, avatar, _id }; /* eslint object-curly-newline: 0 */
const token = jwt.sign(filteredPayload, process.env.SECRET_KEY);
return token;
};
Expand Down
1 change: 1 addition & 0 deletions app/views/includes/foot.jade
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ script(type='text/javascript', src='/js/controllers/index.js')
script(type='text/javascript', src='/js/controllers/scroll.js')
script(type='text/javascript', src='/js/controllers/header.js')
script(type='text/javascript', src='/js/controllers/game.js')
script(type='text/javascript', src='/js/controllers/auth.js')
script(type='text/javascript', src='/js/init.js')


Expand Down
8 changes: 3 additions & 5 deletions backend-test/helpers/tokenizer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ describe('Payload to token converter(Unit Test)', () => {
it(' Should return a valid token that returns to the initial object', () => {
const data = jwt.verify(token, process.env.SECRET_KEY);
expect(data.name).to.equal('A test username');
expect(data.username).to.equal('Hasstrupezekiel');
});

it('should return a token whose payload is an object containing only five fields', () => {
it('should return a token whose payload is an object containing only four fields', () => {
const userObject = jwt.verify(token, process.env.SECRET_KEY);
// testing for five because jwt implicitly adds an 'iat' field to the payload object
expect(Object.keys(userObject).length).to.equal(5);
// testing for four because jwt implicitly adds an 'iat' field to the payload object
expect(Object.keys(userObject).length).to.equal(4);
});
});

Expand Down Expand Up @@ -54,7 +53,6 @@ describe('Payload to token converter(Unit Test)', () => {
it('Should successfully decode a token, returning the initial object sent', () => {
const payLoad = decodeToken(token);
expect(payLoad.name).to.equal('A test username');
expect(payLoad.username).to.equal('Hasstrupezekiel');
});

it('Should throw an error when sent the wrong datatype', () => {
Expand Down
26 changes: 25 additions & 1 deletion backend-test/integration/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'babel-polyfill';
import request from 'supertest';
import faker from 'faker';
import { expect } from 'chai';
import mongoose from 'mongoose';
import app from '../../server';
Expand All @@ -19,7 +20,7 @@ describe('Auth endpoints', () => {
Promise.resolve(User.create(mock));
});

it('POST /api/auth/endpoint should return the user token along with the', (done) => {
it('POST /api/auth/login should return the user token along with the', (done) => {
try {
request(app)
.post('/api/auth/login')
Expand All @@ -35,4 +36,27 @@ describe('Auth endpoints', () => {
expect(err).to.not.exist;
}
});

it('POST /api/auth/signup should return the token of a user on sign up', (done) => {
try {
const fake = {
username: faker.internet.userName(),
name: faker.name.findName(),
email: faker.internet.email(),
password: faker.internet.password(),
};

request(app)
.post('/api/auth/signup')
.send(fake)
.end((err, res) => {
if (err) return done(err);
expect(res.statusCode).to.equal(201);
expect(res.body.token).to.equal(Tokenizer(res.body));
done();
});
} catch (err) {
expect(err).to.not.exist;
}
});
});
3 changes: 2 additions & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"angular-ui-utils": "0.0.4",
"jquery": "~1.9.1",
"underscore": "~1.5.2",
"materialize": "~1.0.0-rc.2"
"materialize": "~1.0.0-rc.2",
"angular-mocks": "~1.7.2"
},
"resolutions": {
"jquery": "^3.0.0 || ^2.1.4"
Expand Down
22 changes: 12 additions & 10 deletions config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import avatars from '../app/controllers/avatars';
import index from '../app/controllers/index';

export default (router, passport, app) => {

// api name spaced routes;
const api = Router();
api
.post('/auth/login', users.handleLogin);
.post('/auth/login', users.handleLogin)
.post('/auth/signup', users.handleSignUp);

router.get('/signin', users.signin);
router.get('/signup', users.signup);
Expand Down Expand Up @@ -96,15 +96,9 @@ export default (router, passport, app) => {
app.use(router);


// refactored to the position to prevent overrides
app.use((req, res) => {
res.status(404).render('404', {
url: req.originalUrl,
error: 'Not found'
});
});

app.use((err, req, res, next) => {
// error from the '/api' namespaced routes
if (err.status) return res.status(err.status).json({ message: err.message });
// Treat as 404
if (err.message.indexOf('not found')) return next();
// Log it
Expand All @@ -114,4 +108,12 @@ export default (router, passport, app) => {
error: err.stack
});
});

// refactored to the position to prevent overrides
app.use((req, res) => {
res.status(404).render('404', {
url: req.originalUrl,
error: 'Not found'
});
});
};
41 changes: 41 additions & 0 deletions frontend-test/angular/auth-controller.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* eslint prefer-arrow-callback: 0, func-names: 0, no-undef: 0 */
describe('AuthController', function () {
let $scope, $location, controller;
beforeEach(module('mean.system'));
mockApireq = function () {
return {
execute(a, b, callback) {
return callback({ token: 'Thisisatesttoken' });
}
};
};
beforeEach(inject(function (_$controller_, _$rootScope_, _$location_) {
// assining providers to global scope
$scope = _$rootScope_.$new();
$location = _$location_;
controller = _$controller_;
controller('AuthController', {
$scope,
$resource: mockApireq,
$location
});
}));

it('Should sign up the user successfully, setting the token to local storage ater', function () {
$scope.newUser = { name: 'Test User', email: 'test@test', password: 'test123' };
$scope.SignUpUser();
const token = localStorage.getItem('#cfhetusertoken');
expect(token).toEqual('Thisisatesttoken');
expect($location.path()).toBe('/app');
});

it('Should log in the user with the successful data then set the returning token to local storage', function () {
$scope.user = { name: 'Test User', email: 'test@test', password: 'test123' };
// listen for calls to the api
$scope.SignInUser();
const token = localStorage.getItem('#cfhetusertoken');
// check the token now
expect(token).toEqual('Thisisatesttoken');
expect($location.path()).toBe('/app');
});
});
8 changes: 0 additions & 8 deletions frontend-test/angular/sample.test.js

This file was deleted.

17 changes: 6 additions & 11 deletions gulpfile.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import karma from 'karma';
import dotenv from 'dotenv';

import path from 'path';
import shell from 'gulp-shell';

dotenv.config();
const { Server } = karma;
Expand Down Expand Up @@ -73,16 +74,10 @@ gulp.task('export', () => {
// Default task(s).
gulp.task('default', ['develop']);

gulp.task('test:backend', ['compile'], () => gulp.src(['dist/backend-test/**/*.js', '!test/angular/**/*.js'])
.pipe(mocha({
reporter: 'spec',
exit: true,
timeout: 5000,
globals: {
should: require('should')
},
compilers: 'babel-register'
})));
// Backend Test task.
gulp.task('test:backend', shell.task([
'NODE_ENV=test nyc mocha backend-test/**/*.js --exit',
]));

// Frontend test task
gulp.task('test:frontend', (done) => {
Expand Down Expand Up @@ -112,4 +107,4 @@ gulp.task('install', () => bower({
}));

// Test task
gulp.task('test', ['test:backend', 'test:frontend']);
gulp.task('test', ['test:frontend', 'test:backend']);
25 changes: 18 additions & 7 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Karma configuration
// Generated on Wed Jun 27 2018 00:00:16 GMT+0100 (WAT)

/* eslint no-var: 0 */
module.exports = (config) => {
config.set({
var configuration = {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',

Expand All @@ -12,6 +12,9 @@ module.exports = (config) => {

// list of files / patterns to load in the browser
files: [
'public/lib/angular/angular.js',
'public/lib/angular-mocks/angular-mocks.js',
'public/js/**/*.js',
'frontend-test/angular/*.test.js'
],

Expand Down Expand Up @@ -40,20 +43,24 @@ module.exports = (config) => {
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,

// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// // enable / disable watching file and executing tests whenever any file changes
// autoWatch: true,

// plugins: ['karma-phantomjs-launcher'],
// plugins: ['karma-phantomjs-launcher', 'karma-jasmine'],

// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromeHeadless'],
browsers: ['Chrome'],

// you can define custom flags
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
},
Chrome_travis_ci: {
base: 'Chrome',
flags: ['--no-sandbox']
}
},

Expand All @@ -64,5 +71,9 @@ module.exports = (config) => {
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
});
};
if (process.env.TRAVIS) {
configuration.browsers = ['Chrome_travis_ci'];
}
config.set(configuration)
};
Loading

0 comments on commit bc9f620

Please sign in to comment.