2019-07-17 13:28:16 +03:00
|
|
|
const _ = require('lodash');
|
|
|
|
const security = require('../../lib/security');
|
|
|
|
const constants = require('../../lib/constants');
|
2020-04-30 22:26:12 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
|
|
|
const {i18n} = require('../../lib/common');
|
2019-07-17 13:28:16 +03:00
|
|
|
const models = require('../../models');
|
|
|
|
const urlUtils = require('../../lib/url-utils');
|
|
|
|
const mail = require('../mail');
|
|
|
|
|
|
|
|
const tokenSecurity = {};
|
|
|
|
|
|
|
|
function generateToken(email, settingsAPI) {
|
|
|
|
const options = {context: {internal: true}};
|
2020-04-29 18:44:27 +03:00
|
|
|
let dbHash;
|
|
|
|
let token;
|
2019-07-17 13:28:16 +03:00
|
|
|
|
|
|
|
return settingsAPI.read(_.merge({key: 'db_hash'}, options))
|
|
|
|
.then((response) => {
|
|
|
|
dbHash = response.settings[0].value;
|
|
|
|
|
|
|
|
return models.User.getByEmail(email, options);
|
|
|
|
})
|
|
|
|
.then((user) => {
|
|
|
|
if (!user) {
|
2020-04-30 22:26:12 +03:00
|
|
|
throw new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')});
|
2019-07-17 13:28:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
token = security.tokens.resetToken.generateHash({
|
|
|
|
expires: Date.now() + constants.ONE_DAY_MS,
|
|
|
|
email: email,
|
|
|
|
dbHash: dbHash,
|
|
|
|
password: user.get('password')
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
email: email,
|
|
|
|
resetToken: token
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function extractTokenParts(options) {
|
|
|
|
options.data.passwordreset[0].token = security.url.decodeBase64(options.data.passwordreset[0].token);
|
|
|
|
|
|
|
|
const tokenParts = security.tokens.resetToken.extract({
|
|
|
|
token: options.data.passwordreset[0].token
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!tokenParts) {
|
2020-04-30 22:26:12 +03:00
|
|
|
return Promise.reject(new errors.UnauthorizedError({
|
|
|
|
message: i18n.t('errors.api.common.invalidTokenStructure')
|
2019-07-17 13:28:16 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve({options, tokenParts});
|
|
|
|
}
|
|
|
|
|
|
|
|
// @TODO: use brute force middleware (see https://github.com/TryGhost/Ghost/pull/7579)
|
|
|
|
function protectBruteForce({options, tokenParts}) {
|
|
|
|
if (tokenSecurity[`${tokenParts.email}+${tokenParts.expires}`] &&
|
|
|
|
tokenSecurity[`${tokenParts.email}+${tokenParts.expires}`].count >= 10) {
|
2020-04-30 22:26:12 +03:00
|
|
|
return Promise.reject(new errors.NoPermissionError({
|
|
|
|
message: i18n.t('errors.models.user.tokenLocked')
|
2019-07-17 13:28:16 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve({options, tokenParts});
|
|
|
|
}
|
|
|
|
|
|
|
|
function doReset(options, tokenParts, settingsAPI) {
|
|
|
|
let dbHash;
|
|
|
|
|
|
|
|
const data = options.data.passwordreset[0];
|
|
|
|
const resetToken = data.token;
|
|
|
|
const oldPassword = data.oldPassword;
|
|
|
|
const newPassword = data.newPassword;
|
|
|
|
|
|
|
|
return settingsAPI.read(_.merge({key: 'db_hash'}, _.omit(options, 'data')))
|
|
|
|
.then((response) => {
|
|
|
|
dbHash = response.settings[0].value;
|
|
|
|
|
|
|
|
return models.User.getByEmail(tokenParts.email, options);
|
|
|
|
})
|
|
|
|
.then((user) => {
|
|
|
|
if (!user) {
|
2020-04-30 22:26:12 +03:00
|
|
|
throw new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')});
|
2019-07-17 13:28:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
let tokenIsCorrect = security.tokens.resetToken.compare({
|
|
|
|
token: resetToken,
|
|
|
|
dbHash: dbHash,
|
|
|
|
password: user.get('password')
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!tokenIsCorrect) {
|
2020-04-30 22:26:12 +03:00
|
|
|
return Promise.reject(new errors.BadRequestError({
|
|
|
|
message: i18n.t('errors.api.common.invalidTokenStructure')
|
2019-07-17 13:28:16 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return models.User.changePassword({
|
|
|
|
oldPassword: oldPassword,
|
|
|
|
newPassword: newPassword,
|
|
|
|
user_id: user.id
|
|
|
|
}, options);
|
|
|
|
})
|
|
|
|
.then((updatedUser) => {
|
|
|
|
updatedUser.set('status', 'active');
|
|
|
|
return updatedUser.save(options);
|
|
|
|
})
|
2020-04-30 22:26:12 +03:00
|
|
|
.catch(errors.ValidationError, (err) => {
|
2019-07-17 13:28:16 +03:00
|
|
|
return Promise.reject(err);
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
2020-04-30 22:26:12 +03:00
|
|
|
if (errors.utils.isIgnitionError(err)) {
|
2019-07-17 13:28:16 +03:00
|
|
|
return Promise.reject(err);
|
|
|
|
}
|
2020-04-30 22:26:12 +03:00
|
|
|
return Promise.reject(new errors.UnauthorizedError({err: err}));
|
2019-07-17 13:28:16 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function sendResetNotification(data, mailAPI) {
|
|
|
|
const adminUrl = urlUtils.urlFor('admin', true);
|
|
|
|
const resetUrl = urlUtils.urlJoin(adminUrl, 'reset', security.url.encodeBase64(data.resetToken), '/');
|
|
|
|
|
|
|
|
const content = await mail.utils.generateContent({
|
|
|
|
data: {
|
|
|
|
resetUrl: resetUrl
|
|
|
|
},
|
|
|
|
template: 'reset-password'
|
|
|
|
});
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
mail: [{
|
|
|
|
message: {
|
|
|
|
to: data.email,
|
2020-04-30 22:26:12 +03:00
|
|
|
subject: i18n.t('common.api.authentication.mail.resetPassword'),
|
2019-07-17 13:28:16 +03:00
|
|
|
html: content.html,
|
|
|
|
text: content.text
|
|
|
|
},
|
|
|
|
options: {}
|
|
|
|
}]
|
|
|
|
};
|
|
|
|
|
|
|
|
return mailAPI.send(payload, {context: {internal: true}});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
generateToken: generateToken,
|
|
|
|
extractTokenParts: extractTokenParts,
|
|
|
|
protectBruteForce: protectBruteForce,
|
|
|
|
doReset: doReset,
|
|
|
|
sendResetNotification: sendResetNotification
|
|
|
|
};
|