Skip to content

Instantly share code, notes, and snippets.

@adnan-i
Created December 29, 2017 22:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adnan-i/03c3a54b65420f615311cbd00abd0e3c to your computer and use it in GitHub Desktop.
Save adnan-i/03c3a54b65420f615311cbd00abd0e3c to your computer and use it in GitHub Desktop.
Example hapi controller
const Boom = require('boom');
const BaseController = require('../../core/abstract/BaseApiController');
class UsersController extends BaseController {
constructor(...args) {
super(...args);
this.User = this.server.plugins.users.User;
this.UserService = this.server.plugins.users.UserService;
}
findById(req, reply) {
return Promise.resolve()
.then(() => {
return this.User.findOne({
where: { id: req.params.id },
include: ['Shops']
});
})
.then(reply);
}
findAll(req, reply) {
return Promise.resolve()
.then(() => {
const q = req.query;
const options = {
limit: q.limit || 100,
offset: q.offset || 0,
order: this.UtilityService.getOrderClause(q.sort),
where: this.UtilityService.getSearchClause(q.search, {Model: this.User}),
};
return this.User.scope('role:user').findAndCountAll(options);
})
.then(reply);
}
create(req, reply) {
return Promise.resolve()
.then(() => {
return this.UserService.create(req.payload);
})
.then(reply);
}
update(req, reply) {
return Promise.resolve()
.then(() => {
const where = {
id: req.auth.credentials.id,
};
if (req.auth.credentials.isAdmin) {
where.id = req.params.id;
}
let emailChanged;
let phoneChanged;
return this.User.findOne({ where })
.then((user) => {
if (!user) throw Boom.notFound('Record not found');
user.set(req.payload);
emailChanged = user.changed('email');
phoneChanged = user.changed('phone');
return user.save();
})
.tap((user) => {
if (!emailChanged) return;
this.EmailVerificationService.sendVerificationEmail(user.email)
.catch((err) => {
this.logger.error(err);
});
})
// .tap((user) => {
// if (!phoneChanged) return;
//
// this.PhoneVerificationService.sendVerificationMessage(user)
// .catch((err) => {
// this.logger.error(err);
// });
// })
.then((user) => {
return this.User.scope('public').findById(user.id);
});
})
.then(reply);
}
register(req, reply) {
return Promise.resolve()
.then(() => {
return this.RecaptchaService.validate(req.payload.recaptcha, req.info.remoteAddress);
})
.then(() => this.User.findOne({where: {email: req.payload.user.email}}))
.then((existingUser) => {
if(existingUser) {
throw Boom.badData('This email is already registered.');
}
})
.then(() => this.UserService.create(req.payload.user))
.then(() => {
// We're capturing errors on this because this is not critical
return this.EmailVerificationService.getVerificationLink(req.payload.user.email)
.then((emailVerificationLink) => {
const tplParams = { emailVerificationLink };
const html = this.MailTemplateService.getCompiledTemplate('welcomeAndVerifyEmail', tplParams);
return this.MailService.send({
to: req.payload.user.email,
subject: 'Verify account email',
html
});
})
.catch((err) => {
this.logger.error(err);
});
})
.then(() => reply.ok());
}
login(req, reply) {
return this.UserService.login(req.payload)
.then(reply);
}
getCurrentUser(req, reply) {
return this.User.scope('public').findOne({
where: { id: req.auth.credentials.id },
include: [{
model: this.server.plugins.shops.Shop.scope(['active']),
required: false,
}],
})
.then(reply);
}
verifyEmail(req, reply) {
return Promise.resolve()
.then(() => {
const hash = req.query.h;
return this.EmailVerificationService.verify(hash);
})
.then((user) => {
user.emailVerified = true;
return user.save();
})
.then(() => reply.ok());
}
resendVerification(req, reply) {
const type = req.params.type;
return Promise.resolve()
.then(() => {
const where = {
id: req.auth.credentials.id,
};
if (req.auth.credentials.isAdmin) {
delete where.id;
where.id = req.params.id;
}
return this.User.findOne({ where });
})
.then((user) => {
if (!user) throw Boom.notFound('Record not found');
if (type === 'email') {
if (user.emailVerified) throw Boom.conflict('Email already verified');
return this.EmailVerificationService.sendVerificationEmail(user.email);
}
if (type === 'phone') {
// TODO: Implement resend verification for phone numbers
throw new Error('Not implemented');
}
})
.then(reply);
}
loginAs(req, reply) {
return Promise.resolve()
.then(() => {
return this.User.findOne({
where: { id: req.params.id },
include: [{
model: this.server.plugins.shops.Shop.scope(['active']),
required: false
}],
})
.then((user) => {
if (!user) throw Boom.notFound('User not found');
return this.UserService.generateJWTToken(user)
.then((token) => {
return { token, user };
});
});
})
.then(reply);
}
resetPasswordRequest(req, reply) {
return Promise.resolve()
.then(() => {
return this.RecaptchaService.validate(req.payload.recaptcha, req.info.remoteAddress);
})
.then(() => this.User.findOne({ where: { email: req.payload.email } }))
.then((user) => {
if (!user) {
throw Boom.badRequest('Unknown email.');
}
user.setResetPasswordToken();
return user.save();
})
.then((user) => {
const tplParams = {
resetPasswordLink: this.UtilityService.getResetPasswordLink(user),
user
};
const html = this.MailTemplateService.getCompiledTemplate('resetPassword', tplParams);
return this.MailService.send({
to: user.email,
subject: 'Reset account password',
html
});
})
.then(() => reply.ok());
}
resetPasswordSubmit(req, reply) {
return Promise.resolve()
.then(() => {
return this.RecaptchaService.validate(req.payload.recaptcha, req.info.remoteAddress);
})
.then(() => this.User.findOne({ where: { resetPasswordToken: req.payload.token } }))
.then((user) => {
if (!user) {
throw Boom.badRequest('Invalid token');
}
user.validateResetToken(req.payload.token);
user.password = req.payload.password;
return user.save();
})
.catch((err) => {
throw err.isBoom ? err : Boom.badRequest(err);
})
.then(() => reply.ok());
}
destroy(req, reply) {
return Promise.resolve()
.then(() => {
return this.User.findById(req.params.id)
.then((user) => {
if (!user) {
throw Boom.notFound('Record not found');
}
return user.destroy();
});
})
.then(reply);
}
}
module.exports = (server) => new UsersController(server);
const _ = require('lodash');
const path = '/users';
const chance = new require('chance')();
const Boom = require('boom');
describe(`Users routes`, () => {
let server;
let apiPrefix;
let User;
before(() => {
return testHelpers.createServer('api')
.then((_server) => {
server = _server;
apiPrefix = server.app.config.get('/app/apiPrefix');
User = server.plugins.users.User;
})
.then(() => testHelpers.migrateUp());
});
beforeEach(() => {
return testHelpers.seedDown()
.then(() => testHelpers.seedUp())
});
describe(`GET ${path}`, () => {
it('should return 401 if not logged in', () => {
return server.send({
method: 'GET',
url: `${apiPrefix}${path}`,
})
.then((res) => {
expect(res.statusCode).to.equal(401);
});
});
it('should return 403 if not an admin', () => {
return testHelpers.getUserWithToken({role: 'user'})
.then((rs) => {
return server.send({
method: 'GET',
url: `${apiPrefix}${path}`,
headers: {Authorization: rs.token}
});
})
.then((res) => {
expect(res.statusCode).to.equal(403);
});
});
it('should return paginated list of users, to admins', () => {
return testHelpers.getUserWithToken({role: 'admin'})
.then((rs) => {
return server.send({
method: 'GET',
url: `${apiPrefix}${path}`,
headers: {Authorization: rs.token}
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.count).to.be.a('number');
expect(res.body.rows).to.be.an('array');
});
});
});
describe(`GET ${path}/{id}`, () => {
it('should return 401 if not logged in', () => {
return server.send({
method: 'GET',
url: `${apiPrefix}${path}/1`,
})
.then((res) => {
expect(res.statusCode).to.equal(401);
});
});
it('should return 403 if not an admin', () => {
return testHelpers.getUserWithToken({role: 'user'})
.then((u) => {
return server.send({
method: 'GET',
url: `${apiPrefix}${path}/1`,
headers: {Authorization: u.token}
});
})
.then((res) => {
expect(res.statusCode).to.equal(403);
});
});
it('should return user that matches id', () => {
return testHelpers.getUserWithToken({role: 'admin'})
.then((u) => {
return testHelpers.getSampleRecord('User')
.then((user) => {
expect(user.id).to.exist();
return server.send({
method: 'GET',
url: `${apiPrefix}${path}/${user.id}`,
headers: {Authorization: u.token}
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body.id).to.equal(user.id);
expect(res.body.email).to.equal(user.email);
});
});
});
});
});
describe(`POST ${path}`, () => {
it('should return 401 if not logged in', () => {
return server.send({
method: 'POST',
url: `${apiPrefix}${path}`,
})
.then((res) => {
expect(res.statusCode).to.equal(401);
});
});
it('should return 403 if not an admin', () => {
return testHelpers.getUserWithToken({role: 'user'})
.then((rs) => {
return server.send({
method: 'POST',
url: `${apiPrefix}${path}`,
headers: {Authorization: rs.token}
});
})
.then((res) => {
expect(res.statusCode).to.equal(403);
});
});
it('should not allow access unless admin', () => {
const payload = testHelpers.getSampleFixture('users', 1, {role: 'user'});
payload.email = chance.email();
return testHelpers.getUserWithToken({role: 'admin'})
.then((rs) => {
return server.send({
method: 'POST',
url: `${apiPrefix}${path}`,
headers: {Authorization: rs.token},
payload
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.firstName).to.equal(payload.firstName);
expect(res.body.email).to.equal(payload.email);
expect(res.body.role).to.equal('user');
return User.destroy({where: {email: payload.email}});
});
});
});
describe(`POST ${path}/register`, () => {
it('should validate the recaptcha token', () => {
const captchaError = Boom.badData('Invalid captcha');
const validateStub = box.stub(server.plugins.core.RecaptchaService, 'validate').rejects(captchaError);
const sampleUser = testHelpers.getSampleFixture('users');
const payload = {
user: sampleUser,
recaptcha: 'string'
};
return server.send({
method: 'POST',
url: `${apiPrefix}${path}/register`,
payload
})
.then((res) => {
expect(res.statusCode).to.equal(422);
expect(res.body.message).to.equal('Invalid captcha');
expect(validateStub.called).to.be.true();
});
});
it('should create the user and send the "welcomeAndVerifyEmail" email', () => {
box.stub(server.plugins.core.RecaptchaService, 'validate').resolves();
const mailSendStub = box.stub(server.plugins.mail.MailService, 'send').resolves();
const getCompiledTemplateSpy = box.spy(server.plugins.mail.MailTemplateService, 'getCompiledTemplate');
const sampleUser = testHelpers.getSampleFixture('users');
sampleUser.email = chance.email();
const payload = {
user: sampleUser,
recaptcha: 'string'
};
return server.send({
method: 'POST',
url: `${apiPrefix}${path}/register`,
payload
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(mailSendStub.called).to.be.true();
const args = mailSendStub.args[0][0];
expect(args).to.be.an('object');
expect(args.to).to.equal(sampleUser.email);
expect(args.subject).to.equal('Verify account email');
expect(getCompiledTemplateSpy.called).to.be.true();
expect(getCompiledTemplateSpy.calledWith('welcomeAndVerifyEmail')).to.be.true();
const tplParams = getCompiledTemplateSpy.args[0][1];
expect(tplParams).to.be.an('object');
const verifyLink = tplParams.emailVerificationLink;
expect(verifyLink).to.exist();
expect(args.html).to.include(verifyLink);
})
.then(() => {
// We should be able to login with newly created user
return server.send({
method: 'POST',
url: `${apiPrefix}/login`,
payload: sampleUser
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.token).to.exist();
expect(res.body.user).to.be.an('object');
expect(res.body.user.emailVerified).to.be.false();
return User.destroy({where: {email: sampleUser.email}});
});
});
});
describe(`POST ${path}/verify-email`, () => {
it('should reject if "h" query param is missing', () => {
return server.send({
method: 'POST',
url: `${apiPrefix}${path}/verify-email`,
})
.then((res) => {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.include('"h" is required');
});
});
it('should verify the hash and set user.emailVerified to true', () => {
box.stub(server.plugins.core.RecaptchaService, 'validate').resolves();
box.stub(server.plugins.mail.MailService, 'send').resolves();
const getCompiledTemplateSpy = box.spy(server.plugins.mail.MailTemplateService, 'getCompiledTemplate');
const sampleUser = testHelpers.getSampleFixture('users');
sampleUser.email = chance.email();
const payload = {
user: sampleUser,
recaptcha: 'string'
};
return server.send({
method: 'POST',
url: `${apiPrefix}${path}/register`,
payload
})
.then((res) => {
expect(res.statusCode).to.equal(200);
return server.plugins.users.User.findOne({where: {email: sampleUser.email}});
})
.then((user) => {
expect(user).to.exist();
expect(user.email).to.equal(sampleUser.email);
expect(user.emailVerified).to.be.false();
const tplParams = getCompiledTemplateSpy.args[0][1];
const verifyLink = tplParams.emailVerificationLink;
expect(verifyLink).to.exist();
const hash = verifyLink.split('?h=')[1];
expect(hash).to.exist();
return server.send({
method: 'POST',
url: `${apiPrefix}${path}/verify-email?h=${hash}`,
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
return server.plugins.users.User.findOne({where: {email: sampleUser.email}});
})
.then((user) => {
expect(user).to.exist();
expect(user.email).to.equal(sampleUser.email);
expect(user.emailVerified).to.be.true();
return User.destroy({where: {email: sampleUser.email}});
});
});
});
describe(`POST /login`, () => {
it('should allow user to login', () => {
const sampleUser = testHelpers.getSampleFixture('users');
return server.send({
method: 'POST',
url: `${apiPrefix}/login`,
payload: {
email: sampleUser.email,
password: sampleUser.password,
}
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.token).to.exist();
});
});
it('should return a token that allows user to access protected APIs', () => {
const sampleUser = testHelpers.getSampleFixture('users');
return server.send({
method: 'GET',
url: `${apiPrefix}${path}/me`,
})
.then((res) => {
// Should fail because nobody is logged in
expect(res.statusCode).to.equal(401);
// Now login the user
return server.send({
method: 'POST',
url: `${apiPrefix}/login`,
payload: {
email: sampleUser.email,
password: sampleUser.password,
}
});
})
.then((res) => {
expect(res.body.token).to.exist();
return server.send({
method: 'GET',
url: `${apiPrefix}${path}/me`,
headers: {Authorization: res.body.token},
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
});
});
it('should return user object alongside the token', () => {
const sampleUser = testHelpers.getSampleFixture('users');
return server.send({
method: 'POST',
url: `${apiPrefix}/login`,
payload: {
email: sampleUser.email,
password: sampleUser.password,
}
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.token).to.exist();
expect(res.body.user).to.exist();
expect(res.body.user).to.be.an('object');
expect(res.body.user.email).to.equal(sampleUser.email);
});
});
});
describe(`PUT|POST ${path}/{id}`, () => {
it('should allow updating user, when having a valid auth token', () => {
const sampleUser = testHelpers.getSampleFixture('users');
const payload = _.assign({}, sampleUser, {phone: chance.string()});
expect(payload.phone).to.not.equal(sampleUser.phone);
// Should fail because there's no token
return server.send({
method: 'PUT',
url: `${apiPrefix}${path}/${sampleUser.id}`,
payload,
})
.then((res) => {
expect(res.statusCode).to.equal(401);
// Now login the user
return server.send({
method: 'POST',
url: `${apiPrefix}/login`,
payload: {
email: sampleUser.email,
password: sampleUser.password,
}
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.token).to.exist();
// Should succeed because we have a valid token
return server.send({
method: 'PUT',
url: `${apiPrefix}${path}/${sampleUser.id}`,
payload,
headers: {Authorization: res.body.token}
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body.phone).to.equal(payload.phone);
});
});
});
describe(`PUT|POST ${path}/{id}/password`, () => {
it('should allow updating user\'s password', () => {
let user;
const sampleUser = testHelpers.getSampleFixture('users');
const payload = {
password: chance.string()
};
return testHelpers.getSampleRecord('User', {email: sampleUser.email})
.then((_user) => {
user = _user;
return server.send({
method: 'POST',
url: `${apiPrefix}/login`,
payload: {
email: user.email,
password: sampleUser.password,
}
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.token).to.exist();
// Should succeed because we have a valid token
return server.send({
method: 'PUT',
url: `${apiPrefix}${path}/${user.id}/password`,
payload,
headers: {Authorization: res.body.token}
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
// Now login the user with new password
return server.send({
method: 'POST',
url: `${apiPrefix}/login`,
payload: {
email: user.email,
password: payload.password,
}
});
})
.then((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.token).to.exist();
});
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment