Skip to content

Commit

Permalink
Migrated authentication controller to v2 (#10950)
Browse files Browse the repository at this point in the history
refs #10060

- Migrated authentication.resetPassword method to v2
- Migrated authentication.acceptInvitation method to v2
- Migrated authentication.setup method to v2
- Added missing test coverage for "setupUpdate" method
- Migrated authentication.updateSetup method to v2
- Migrated authentication.isInvitation method to v2
- Migrated authentication.isSetup method to v2
- Removed unused 'setup.completed' event as it wasn's used anywhere in the system and has been complicating the logic unnecessarily
- Without the event, it's possible to simplify sendNotification method to just use email address of the user
- Added email sending check to v0.1 test suite
- Refactored sendNotification method to just use email address as parameter
- Renamed sendNotification to sendWelcomeMail
- The only thing the method does now is sending welcome mail, so new naming seems natural :)
  • Loading branch information
naz committed Aug 1, 2019
2 parents 75f6e9c + db9eed6 commit 27bf453
Show file tree
Hide file tree
Showing 15 changed files with 763 additions and 21 deletions.
10 changes: 10 additions & 0 deletions core/server/api/shared/validators/input/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,15 @@ module.exports = {
changePassword() {
debug('validate changePassword');
return this.add(...arguments);
},

resetPassword() {
debug('validate resetPassword');
return this.add(...arguments);
},

setup() {
debug('validate setup');
return this.add(...arguments);
}
};
3 changes: 2 additions & 1 deletion core/server/api/v0.1/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ authentication = {
}

function sendNotification(setupUser) {
return auth.setup.sendNotification(setupUser, mailAPI);
return auth.setup.sendWelcomeEmail(setupUser.email, mailAPI)
.then(() => setupUser);
}

function formatResponse(setupUser) {
Expand Down
186 changes: 186 additions & 0 deletions core/server/api/v2/authentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
const api = require('./index');
const config = require('../../config');
const common = require('../../lib/common');
const web = require('../../web');
const models = require('../../models');
const auth = require('../../services/auth');
const invitations = require('../../services/invitations');

module.exports = {
docName: 'authentication',

setup: {
statusCode: 201,
permissions: false,
validation: {
docName: 'setup'
},
query(frame) {
return Promise.resolve()
.then(() => {
return auth.setup.assertSetupCompleted(false)();
})
.then(() => {
const setupDetails = {
name: frame.data.setup[0].name,
email: frame.data.setup[0].email,
password: frame.data.setup[0].password,
blogTitle: frame.data.setup[0].blogTitle,
status: 'active'
};

return auth.setup.setupUser(setupDetails);
})
.then((data) => {
return auth.setup.doSettings(data, api.settings);
})
.then((user) => {
return auth.setup.sendWelcomeEmail(user.get('email'), api.mail)
.then(() => user);
});
}
},

updateSetup: {
permissions: (frame) => {
return models.User.findOne({role: 'Owner', status: 'all'})
.then((owner) => {
if (owner.id !== frame.options.context.user) {
throw new common.errors.NoPermissionError({message: common.i18n.t('errors.api.authentication.notTheBlogOwner')});
}
});
},
validation: {
docName: 'setup'
},
query(frame) {
return Promise.resolve()
.then(() => {
return auth.setup.assertSetupCompleted(true)();
})
.then(() => {
const setupDetails = {
name: frame.data.setup[0].name,
email: frame.data.setup[0].email,
password: frame.data.setup[0].password,
blogTitle: frame.data.setup[0].blogTitle,
status: 'active'
};

return auth.setup.setupUser(setupDetails);
})
.then((data) => {
return auth.setup.doSettings(data, api.settings);
});
}
},

isSetup: {
permissions: false,
query() {
return auth.setup.checkIsSetup()
.then((isSetup) => {
return {
status: isSetup,
// Pre-populate from config if, and only if the values exist in config.
title: config.title || undefined,
name: config.user_name || undefined,
email: config.user_email || undefined
};
});
}
},

generateResetToken: {
validation: {
docName: 'passwordreset'
},
permissions: true,
options: [
'email'
],
query(frame) {
return Promise.resolve()
.then(() => {
return auth.setup.assertSetupCompleted(true)();
})
.then(() => {
return auth.passwordreset.generateToken(frame.data.passwordreset[0].email, api.settings);
})
.then((token) => {
return auth.passwordreset.sendResetNotification(token, api.mail);
});
}
},

resetPassword: {
validation: {
docName: 'passwordreset',
data: {
newPassword: {required: true},
ne2Password: {required: true}
}
},
permissions: false,
options: [
'ip'
],
query(frame) {
return Promise.resolve()
.then(() => {
return auth.setup.assertSetupCompleted(true)();
})
.then(() => {
return auth.passwordreset.extractTokenParts(frame);
})
.then((params) => {
return auth.passwordreset.protectBruteForce(params);
})
.then(({options, tokenParts}) => {
options = Object.assign(options, {context: {internal: true}});
return auth.passwordreset.doReset(options, tokenParts, api.settings)
.then((params) => {
web.shared.middlewares.api.spamPrevention.userLogin().reset(frame.options.ip, `${tokenParts.email}login`);
return params;
});
});
}
},

acceptInvitation: {
validation: {
docName: 'invitations'
},
permissions: false,
query(frame) {
return Promise.resolve()
.then(() => {
return auth.setup.assertSetupCompleted(true)();
})
.then(() => {
return invitations.accept(frame.data);
});
}
},

isInvitation: {
data: [
'email'
],
validation: {
docName: 'invitations'
},
permissions: false,
query(frame) {
return Promise.resolve()
.then(() => {
return auth.setup.assertSetupCompleted(true)();
})
.then(() => {
const email = frame.data.email;

return models.Invite.findOne({email: email, status: 'sent'}, frame.options);
});
}
}
};
4 changes: 4 additions & 0 deletions core/server/api/v2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module.exports = {
return shared.http;
},

get authentication() {
return shared.pipeline(require('./authentication'), localUtils);
},

get db() {
return shared.pipeline(require('./db'), localUtils);
},
Expand Down
63 changes: 63 additions & 0 deletions core/server/api/v2/utils/serializers/output/authentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const common = require('../../../../../lib/common');
const mapper = require('./utils/mapper');
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:authentication');

module.exports = {
setup(user, apiConfig, frame) {
frame.response = {
users: [
mapper.mapUser(user, {options: {context: {internal: true}}})
]
};
},

updateSetup(user, apiConfig, frame) {
frame.response = {
users: [
mapper.mapUser(user, {options: {context: {internal: true}}})
]
};
},

isSetup(data, apiConfig, frame) {
frame.response = {
setup: [data]
};
},

generateResetToken(data, apiConfig, frame) {
frame.response = {
passwordreset: [{
message: common.i18n.t('common.api.authentication.mail.checkEmailForInstructions')
}]
};
},

resetPassword(data, apiConfig, frame) {
frame.response = {
passwordreset: [{
message: common.i18n.t('common.api.authentication.mail.passwordChanged')
}]
};
},

acceptInvitation(data, apiConfig, frame) {
debug('acceptInvitation');

frame.response = {
invitation: [
{message: common.i18n.t('common.api.authentication.mail.invitationAccepted')}
]
};
},

isInvitation(data, apiConfig, frame) {
debug('acceptInvitation');

frame.response = {
invitation: [{
valid: !!data
}]
};
}
};
4 changes: 4 additions & 0 deletions core/server/api/v2/utils/serializers/output/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ module.exports = {
return require('./all');
},

get authentication() {
return require('./authentication');
},

get db() {
return require('./db');
},
Expand Down
12 changes: 12 additions & 0 deletions core/server/api/v2/utils/validators/input/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
module.exports = {
get passwordreset() {
return require('./passwordreset');
},

get setup() {
return require('./setup');
},

get posts() {
return require('./posts');
},
Expand All @@ -11,6 +19,10 @@ module.exports = {
return require('./invites');
},

get invitations() {
return require('./invitations');
},

get settings() {
return require('./settings');
},
Expand Down
40 changes: 40 additions & 0 deletions core/server/api/v2/utils/validators/input/invitations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const Promise = require('bluebird');
const validator = require('validator');
const debug = require('ghost-ignition').debug('api:v2:utils:validators:input:invitation');
const common = require('../../../../../lib/common');

module.exports = {
acceptInvitation(apiConfig, frame) {
debug('acceptInvitation');

const data = frame.data.invitation[0];

if (!data.token) {
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.authentication.noTokenProvided')}));
}

if (!data.email) {
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.authentication.noEmailProvided')}));
}

if (!data.password) {
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.authentication.noPasswordProvided')}));
}

if (!data.name) {
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.authentication.noNameProvided')}));
}
},

isInvitation(apiConfig, frame) {
debug('isInvitation');

const email = frame.data.email;

if (typeof email !== 'string' || !validator.isEmail(email)) {
throw new common.errors.BadRequestError({
message: common.i18n.t('errors.api.authentication.invalidEmailReceived')
});
}
}
};
30 changes: 30 additions & 0 deletions core/server/api/v2/utils/validators/input/passwordreset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const Promise = require('bluebird');
const validator = require('validator');
const debug = require('ghost-ignition').debug('api:v2:utils:validators:input:passwordreset');
const common = require('../../../../../lib/common');

module.exports = {
resetPassword(apiConfig, frame) {
debug('resetPassword');

const data = frame.data.passwordreset[0];

if (data.newPassword !== data.ne2Password) {
return Promise.reject(new common.errors.ValidationError({
message: common.i18n.t('errors.models.user.newPasswordsDoNotMatch')
}));
}
},

generateResetToken(apiConfig, frame) {
debug('generateResetToken');

const email = frame.data.passwordreset[0].email;

if (typeof email !== 'string' || !validator.isEmail(email)) {
throw new common.errors.BadRequestError({
message: common.i18n.t('errors.api.authentication.invalidEmailReceived')
});
}
}
};

0 comments on commit 27bf453

Please sign in to comment.