Created
December 29, 2017 22:23
-
-
Save adnan-i/03c3a54b65420f615311cbd00abd0e3c to your computer and use it in GitHub Desktop.
Example hapi controller
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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