mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 22:02:11 +03:00
273e220327
refs 829e8ed010
- i18n is used everywhere but only requires shared or external packages, therefore it's a good candidate for living in shared
- this reduces invalid requires across frontend and server, and lets us use it everywhere until we come up with a better option
173 lines
5.7 KiB
JavaScript
173 lines
5.7 KiB
JavaScript
const _ = require('lodash');
|
|
const security = require('@tryghost/security');
|
|
const constants = require('@tryghost/constants');
|
|
const errors = require('@tryghost/errors');
|
|
const i18n = require('../../../shared/i18n');
|
|
const models = require('../../models');
|
|
const urlUtils = require('../../../shared/url-utils');
|
|
const mail = require('../mail');
|
|
|
|
const tokenSecurity = {};
|
|
|
|
function generateToken(email, settingsAPI) {
|
|
const options = {context: {internal: true}};
|
|
let dbHash;
|
|
let token;
|
|
|
|
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) {
|
|
throw new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')});
|
|
}
|
|
|
|
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) {
|
|
return Promise.reject(new errors.UnauthorizedError({
|
|
message: i18n.t('errors.api.passwordReset.corruptedToken.message'),
|
|
context: i18n.t('errors.api.passwordReset.corruptedToken.context'),
|
|
help: i18n.t('errors.api.passwordReset.corruptedToken.help')
|
|
}));
|
|
}
|
|
|
|
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) {
|
|
return Promise.reject(new errors.NoPermissionError({
|
|
message: i18n.t('errors.models.user.tokenLocked')
|
|
}));
|
|
}
|
|
|
|
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) {
|
|
throw new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')});
|
|
}
|
|
|
|
let compareResult = security.tokens.resetToken.compare({
|
|
token: resetToken,
|
|
dbHash: dbHash,
|
|
password: user.get('password')
|
|
});
|
|
|
|
if (!compareResult.correct) {
|
|
let error;
|
|
if (compareResult.reason === 'expired' || compareResult.reason === 'invalid_expiry') {
|
|
error = new errors.BadRequestError({
|
|
message: i18n.t('errors.api.passwordReset.expired.message'),
|
|
context: i18n.t('errors.api.passwordReset.expired.context'),
|
|
help: i18n.t('errors.api.passwordReset.expired.help')
|
|
});
|
|
} else {
|
|
error = new errors.BadRequestError({
|
|
message: i18n.t('errors.api.passwordReset.invalidToken.message'),
|
|
context: i18n.t('errors.api.passwordReset.invalidToken.context'),
|
|
help: i18n.t('errors.api.passwordReset.invalidToken.help')
|
|
});
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
return models.User.changePassword({
|
|
oldPassword: oldPassword,
|
|
newPassword: newPassword,
|
|
user_id: user.id
|
|
}, options);
|
|
})
|
|
.then((updatedUser) => {
|
|
updatedUser.set('status', 'active');
|
|
return updatedUser.save(options);
|
|
})
|
|
.catch(errors.ValidationError, (err) => {
|
|
return Promise.reject(err);
|
|
})
|
|
.catch((err) => {
|
|
if (errors.utils.isIgnitionError(err)) {
|
|
return Promise.reject(err);
|
|
}
|
|
return Promise.reject(new errors.UnauthorizedError({err: err}));
|
|
});
|
|
}
|
|
|
|
async function sendResetNotification(data, mailAPI) {
|
|
const adminUrl = urlUtils.urlFor('admin', true);
|
|
const resetToken = security.url.encodeBase64(data.resetToken);
|
|
const resetUrl = urlUtils.urlJoin(adminUrl, 'reset', resetToken, '/');
|
|
const emailData = {
|
|
resetUrl: resetUrl,
|
|
recipientEmail: data.email
|
|
};
|
|
|
|
const content = await mail.utils.generateContent({
|
|
data: emailData,
|
|
template: 'reset-password'
|
|
});
|
|
|
|
const payload = {
|
|
mail: [{
|
|
message: {
|
|
to: data.email,
|
|
subject: i18n.t('common.api.authentication.mail.resetPassword'),
|
|
html: content.html,
|
|
text: content.text
|
|
},
|
|
options: {}
|
|
}]
|
|
};
|
|
|
|
return mailAPI.send(payload, {context: {internal: true}});
|
|
}
|
|
|
|
module.exports = {
|
|
generateToken,
|
|
extractTokenParts,
|
|
protectBruteForce,
|
|
doReset,
|
|
sendResetNotification
|
|
};
|