mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-01 23:37:43 +03:00
parent
244a444e38
commit
fe13503470
@ -1,54 +1,125 @@
|
||||
var _ = require('lodash'),
|
||||
validator = require('validator'),
|
||||
pipeline = require('../utils/pipeline'),
|
||||
dataProvider = require('../models'),
|
||||
settings = require('./settings'),
|
||||
mail = require('./mail'),
|
||||
globalUtils = require('../utils'),
|
||||
utils = require('./utils'),
|
||||
Promise = require('bluebird'),
|
||||
errors = require('../errors'),
|
||||
config = require('../config'),
|
||||
i18n = require('../i18n'),
|
||||
authentication;
|
||||
|
||||
function setupTasks(object) {
|
||||
var setupUser,
|
||||
internal = {context: {internal: true}};
|
||||
|
||||
return utils.checkObject(object, 'setup').then(function (checkedSetupData) {
|
||||
setupUser = {
|
||||
name: checkedSetupData.setup[0].name,
|
||||
email: checkedSetupData.setup[0].email,
|
||||
password: checkedSetupData.setup[0].password,
|
||||
blogTitle: checkedSetupData.setup[0].blogTitle,
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
return dataProvider.User.findOne({role: 'Owner', status: 'all'});
|
||||
}).then(function (ownerUser) {
|
||||
if (ownerUser) {
|
||||
return dataProvider.User.setup(setupUser, _.extend({id: ownerUser.id}, internal));
|
||||
} else {
|
||||
return dataProvider.Role.findOne({name: 'Owner'}).then(function (ownerRole) {
|
||||
setupUser.roles = [ownerRole.id];
|
||||
return dataProvider.User.add(setupUser, internal);
|
||||
});
|
||||
}
|
||||
}).then(function (user) {
|
||||
var userSettings = [];
|
||||
|
||||
// Handles the additional values set by the setup screen.
|
||||
if (!_.isEmpty(setupUser.blogTitle)) {
|
||||
userSettings.push({key: 'title', value: setupUser.blogTitle});
|
||||
userSettings.push({key: 'description', value: i18n.t('common.api.authentication.sampleBlogDescription')});
|
||||
}
|
||||
|
||||
setupUser = user.toJSON(internal);
|
||||
return settings.edit({settings: userSettings}, {context: {user: setupUser.id}});
|
||||
}).then(function () {
|
||||
return Promise.resolve(setupUser);
|
||||
/**
|
||||
* Returns setup status
|
||||
*
|
||||
* @return {Promise<Boolean>}
|
||||
*/
|
||||
function checkSetup() {
|
||||
return authentication.isSetup().then(function then(result) {
|
||||
return result.setup[0].status;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows an assertion to be made about setup status.
|
||||
*
|
||||
* @param {Boolean} status True: setup must be complete. False: setup must not be complete.
|
||||
* @return {Function} returns a "task ready" function
|
||||
*/
|
||||
function assertSetupCompleted(status) {
|
||||
return function checkPermission(__) {
|
||||
return checkSetup().then(function then(isSetup) {
|
||||
if (isSetup === status) {
|
||||
return __;
|
||||
}
|
||||
|
||||
var completed = i18n.t('errors.api.authentication.setupAlreadyCompleted'),
|
||||
notCompleted = i18n.t('errors.api.authentication.setupMustBeCompleted');
|
||||
|
||||
function throwReason(reason) {
|
||||
throw new errors.NoPermissionError(reason);
|
||||
}
|
||||
|
||||
if (isSetup) {
|
||||
throwReason(completed);
|
||||
} else {
|
||||
throwReason(notCompleted);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setupTasks(setupData) {
|
||||
var tasks;
|
||||
|
||||
function validateData(setupData) {
|
||||
return utils.checkObject(setupData, 'setup').then(function then(checked) {
|
||||
var data = checked.setup[0];
|
||||
|
||||
return {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
blogTitle: data.blogTitle,
|
||||
status: 'active'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setupUser(userData) {
|
||||
var context = {context: {internal: true}},
|
||||
User = dataProvider.User;
|
||||
|
||||
return User.findOne({role: 'Owner', status: 'all'}).then(function then(owner) {
|
||||
if (!owner) {
|
||||
throw new errors.InternalServerError(
|
||||
i18n.t('errors.api.authentication.setupUnableToRun')
|
||||
);
|
||||
}
|
||||
|
||||
return User.setup(userData, _.extend({id: owner.id}, context));
|
||||
}).then(function then(user) {
|
||||
return {
|
||||
user: user,
|
||||
userData: userData
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function doSettings(data) {
|
||||
var user = data.user,
|
||||
blogTitle = data.userData.blogTitle,
|
||||
context = {context: {user: data.user.id}},
|
||||
userSettings;
|
||||
|
||||
if (!blogTitle || typeof blogTitle !== 'string') {
|
||||
return user;
|
||||
}
|
||||
|
||||
userSettings = [
|
||||
{key: 'title', value: blogTitle.trim()},
|
||||
{key: 'description', value: i18n.t('common.api.authentication.sampleBlogDescription')}
|
||||
];
|
||||
|
||||
return settings.edit({settings: userSettings}, context).return(user);
|
||||
}
|
||||
|
||||
function formatResponse(user) {
|
||||
return user.toJSON({context: {internal: true}});
|
||||
}
|
||||
|
||||
tasks = [
|
||||
validateData,
|
||||
setupUser,
|
||||
doSettings,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return pipeline(tasks, setupData);
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Authentication API Methods
|
||||
*
|
||||
@ -57,266 +128,420 @@ function setupTasks(object) {
|
||||
authentication = {
|
||||
|
||||
/**
|
||||
* ## Generate Reset Token
|
||||
* generate a reset token for a given email address
|
||||
* @param {Object} object
|
||||
* @returns {Promise(passwordreset)} message
|
||||
* @description generate a reset token for a given email address
|
||||
* @param {Object} resetRequest
|
||||
* @returns {Promise<Object>} message
|
||||
*/
|
||||
generateResetToken: function generateResetToken(object) {
|
||||
var expires = Date.now() + globalUtils.ONE_DAY_MS,
|
||||
email;
|
||||
generateResetToken: function generateResetToken(resetRequest) {
|
||||
var tasks;
|
||||
|
||||
return authentication.isSetup().then(function (result) {
|
||||
var setup = result.setup[0].status;
|
||||
function validateRequest(resetRequest) {
|
||||
return utils.checkObject(resetRequest, 'passwordreset').then(function then(data) {
|
||||
var email = data.passwordreset[0].email;
|
||||
|
||||
if (!setup) {
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
|
||||
}
|
||||
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
||||
throw new errors.BadRequestError(
|
||||
i18n.t('errors.api.authentication.noEmailProvided')
|
||||
);
|
||||
}
|
||||
|
||||
return utils.checkObject(object, 'passwordreset');
|
||||
}).then(function (checkedPasswordReset) {
|
||||
if (checkedPasswordReset.passwordreset[0].email) {
|
||||
email = checkedPasswordReset.passwordreset[0].email;
|
||||
} else {
|
||||
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.authentication.noEmailProvided')));
|
||||
}
|
||||
return email;
|
||||
});
|
||||
}
|
||||
|
||||
return settings.read({context: {internal: true}, key: 'dbHash'})
|
||||
.then(function (response) {
|
||||
var dbHash = response.settings[0].value;
|
||||
return dataProvider.User.generateResetToken(email, expires, dbHash);
|
||||
}).then(function (resetToken) {
|
||||
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
|
||||
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + globalUtils.encodeBase64URLsafe(resetToken) + '/';
|
||||
function generateToken(email) {
|
||||
var settingsQuery = {context: {internal: true}, key: 'dbHash'};
|
||||
|
||||
return mail.generateContent({data: {resetUrl: resetUrl}, template: 'reset-password'});
|
||||
}).then(function (emailContent) {
|
||||
return settings.read(settingsQuery).then(function then(response) {
|
||||
var dbHash = response.settings[0].value,
|
||||
expiresAt = Date.now() + globalUtils.ONE_DAY_MS;
|
||||
|
||||
return dataProvider.User.generateResetToken(email, expiresAt, dbHash);
|
||||
}).then(function then(resetToken) {
|
||||
return {
|
||||
email: email,
|
||||
resetToken: resetToken
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function sendResetNotification(data) {
|
||||
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
|
||||
resetUrl = baseUrl.replace(/\/$/, '') +
|
||||
'/ghost/reset/' +
|
||||
globalUtils.encodeBase64URLsafe(data.resetToken) + '/';
|
||||
|
||||
return mail.generateContent({
|
||||
data: {
|
||||
resetUrl: resetUrl
|
||||
},
|
||||
template: 'reset-password'
|
||||
}).then(function then(content) {
|
||||
var payload = {
|
||||
mail: [{
|
||||
message: {
|
||||
to: email,
|
||||
to: data.email,
|
||||
subject: i18n.t('common.api.authentication.mail.resetPassword'),
|
||||
html: emailContent.html,
|
||||
text: emailContent.text
|
||||
html: content.html,
|
||||
text: content.text
|
||||
},
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
|
||||
return mail.send(payload, {context: {internal: true}});
|
||||
}).then(function () {
|
||||
return Promise.resolve({passwordreset: [{message: i18n.t('common.api.authentication.mail.checkEmailForInstructions')}]});
|
||||
}).catch(function (error) {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatResponse() {
|
||||
return {
|
||||
passwordreset: [
|
||||
{message: i18n.t('common.api.authentication.mail.checkEmailForInstructions')}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
tasks = [
|
||||
assertSetupCompleted(true),
|
||||
validateRequest,
|
||||
generateToken,
|
||||
sendResetNotification,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return pipeline(tasks, resetRequest);
|
||||
},
|
||||
|
||||
/**
|
||||
* ## Reset Password
|
||||
* reset password if a valid token and password (2x) is passed
|
||||
* @param {Object} object
|
||||
* @returns {Promise(passwordreset)} message
|
||||
* @param {Object} resetRequest
|
||||
* @returns {Promise<Object>} message
|
||||
*/
|
||||
resetPassword: function resetPassword(object) {
|
||||
var resetToken,
|
||||
newPassword,
|
||||
ne2Password;
|
||||
resetPassword: function resetPassword(resetRequest) {
|
||||
var tasks;
|
||||
|
||||
return authentication.isSetup().then(function (result) {
|
||||
var setup = result.setup[0].status;
|
||||
function validateRequest(resetRequest) {
|
||||
return utils.checkObject(resetRequest, 'passwordreset');
|
||||
}
|
||||
|
||||
if (!setup) {
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
|
||||
}
|
||||
function doReset(resetRequest) {
|
||||
var settingsQuery = {context: {internal: true}, key: 'dbHash'},
|
||||
data = resetRequest.passwordreset[0],
|
||||
resetToken = data.token,
|
||||
newPassword = data.newPassword,
|
||||
ne2Password = data.ne2Password;
|
||||
|
||||
return utils.checkObject(object, 'passwordreset');
|
||||
}).then(function (checkedPasswordReset) {
|
||||
resetToken = checkedPasswordReset.passwordreset[0].token;
|
||||
newPassword = checkedPasswordReset.passwordreset[0].newPassword;
|
||||
ne2Password = checkedPasswordReset.passwordreset[0].ne2Password;
|
||||
|
||||
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) {
|
||||
var dbHash = response.settings[0].value;
|
||||
return settings.read(settingsQuery).then(function then(response) {
|
||||
return dataProvider.User.resetPassword({
|
||||
token: resetToken,
|
||||
newPassword: newPassword,
|
||||
ne2Password: ne2Password,
|
||||
dbHash: dbHash
|
||||
dbHash: response.settings[0].value
|
||||
});
|
||||
}).then(function () {
|
||||
return Promise.resolve({passwordreset: [{message: i18n.t('common.api.authentication.mail.passwordChanged')}]});
|
||||
}).catch(function (error) {
|
||||
return Promise.reject(new errors.UnauthorizedError(error.message));
|
||||
throw new errors.UnauthorizedError(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatResponse() {
|
||||
return {
|
||||
passwordreset: [
|
||||
{message: i18n.t('common.api.authentication.mail.passwordChanged')}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
tasks = [
|
||||
assertSetupCompleted(true),
|
||||
validateRequest,
|
||||
doReset,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return pipeline(tasks, resetRequest);
|
||||
},
|
||||
|
||||
/**
|
||||
* ### Accept Invitation
|
||||
* @param {User} object the user to create
|
||||
* @returns {Promise(User}} Newly created user
|
||||
* @param {Object} invitation an invitation object
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
acceptInvitation: function acceptInvitation(object) {
|
||||
var resetToken,
|
||||
newPassword,
|
||||
ne2Password,
|
||||
name,
|
||||
email;
|
||||
acceptInvitation: function acceptInvitation(invitation) {
|
||||
var tasks;
|
||||
|
||||
return authentication.isSetup().then(function (result) {
|
||||
var setup = result.setup[0].status;
|
||||
function validateInvitation(invitation) {
|
||||
return utils.checkObject(invitation, 'invitation');
|
||||
}
|
||||
|
||||
if (!setup) {
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
|
||||
}
|
||||
function processInvitation(invitation) {
|
||||
var User = dataProvider.User,
|
||||
settingsQuery = {context: {internal: true}, key: 'dbHash'},
|
||||
data = invitation.invitation[0],
|
||||
resetToken = data.token,
|
||||
newPassword = data.password,
|
||||
email = data.email,
|
||||
name = data.name;
|
||||
|
||||
return utils.checkObject(object, 'invitation');
|
||||
}).then(function (checkedInvitation) {
|
||||
resetToken = checkedInvitation.invitation[0].token;
|
||||
newPassword = checkedInvitation.invitation[0].password;
|
||||
ne2Password = checkedInvitation.invitation[0].password;
|
||||
email = checkedInvitation.invitation[0].email;
|
||||
name = checkedInvitation.invitation[0].name;
|
||||
|
||||
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) {
|
||||
var dbHash = response.settings[0].value;
|
||||
return dataProvider.User.resetPassword({
|
||||
return settings.read(settingsQuery).then(function then(response) {
|
||||
return User.resetPassword({
|
||||
token: resetToken,
|
||||
newPassword: newPassword,
|
||||
ne2Password: ne2Password,
|
||||
dbHash: dbHash
|
||||
ne2Password: newPassword,
|
||||
dbHash: response.settings[0].value
|
||||
});
|
||||
}).then(function (user) {
|
||||
// Setting the slug to '' has the model regenerate the slug from the user's name
|
||||
return dataProvider.User.edit({name: name, email: email, slug: ''}, {id: user.id});
|
||||
}).then(function () {
|
||||
return Promise.resolve({invitation: [{message: i18n.t('common.api.authentication.mail.invitationAccepted')}]});
|
||||
}).then(function then(user) {
|
||||
return User.edit({name: name, email: email, slug: ''}, {id: user.id});
|
||||
}).catch(function (error) {
|
||||
return Promise.reject(new errors.UnauthorizedError(error.message));
|
||||
throw new errors.UnauthorizedError(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatResponse() {
|
||||
return {
|
||||
invitation: [
|
||||
{message: i18n.t('common.api.authentication.mail.invitationAccepted')}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
tasks = [
|
||||
assertSetupCompleted(true),
|
||||
validateInvitation,
|
||||
processInvitation,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return pipeline(tasks, invitation);
|
||||
},
|
||||
|
||||
/**
|
||||
* ### Check for invitation
|
||||
* @param {Object} options
|
||||
* @param {string} options.email The email to check for an invitation on
|
||||
* @returns {Promise(Invitation}} An invitation status
|
||||
* @returns {Promise<Object>} An invitation status
|
||||
*/
|
||||
isInvitation: function isInvitation(options) {
|
||||
return authentication.isSetup().then(function (result) {
|
||||
var setup = result.setup[0].status;
|
||||
var tasks,
|
||||
localOptions = _.cloneDeep(options || {});
|
||||
|
||||
if (!setup) {
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
|
||||
function processArgs(options) {
|
||||
var email = options.email;
|
||||
|
||||
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
||||
throw new errors.BadRequestError(
|
||||
i18n.t('errors.api.authentication.invalidEmailReceived')
|
||||
);
|
||||
}
|
||||
|
||||
if (options.email) {
|
||||
return dataProvider.User.findOne({email: options.email, status: 'invited'}).then(function (response) {
|
||||
if (response) {
|
||||
return {invitation: [{valid: true}]};
|
||||
} else {
|
||||
return {invitation: [{valid: false}]};
|
||||
}
|
||||
return email;
|
||||
}
|
||||
|
||||
function checkInvitation(email) {
|
||||
return dataProvider.User
|
||||
.where({email: email, status: 'invited'})
|
||||
.count('id')
|
||||
.then(function then(count) {
|
||||
return !!count;
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.authentication.invalidEmailReceived')));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatResponse(isInvited) {
|
||||
return {invitation: [{valid: isInvited}]};
|
||||
}
|
||||
|
||||
tasks = [
|
||||
processArgs,
|
||||
assertSetupCompleted(true),
|
||||
checkInvitation,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return pipeline(tasks, localOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks the setup status
|
||||
* @return {Promise}
|
||||
*/
|
||||
isSetup: function isSetup() {
|
||||
return dataProvider.User.query(function (qb) {
|
||||
qb.whereIn('status', ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked']);
|
||||
}).fetch().then(function (users) {
|
||||
if (users) {
|
||||
return Promise.resolve({setup: [{status: true}]});
|
||||
} else {
|
||||
return Promise.resolve({setup: [{status: false}]});
|
||||
}
|
||||
});
|
||||
var tasks,
|
||||
validStatuses = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'];
|
||||
|
||||
function checkSetupStatus() {
|
||||
return dataProvider.User
|
||||
.where('status', 'in', validStatuses)
|
||||
.count('id')
|
||||
.then(function (count) {
|
||||
return !!count;
|
||||
});
|
||||
}
|
||||
|
||||
function formatResponse(isSetup) {
|
||||
return {setup: [{status: isSetup}]};
|
||||
}
|
||||
|
||||
tasks = [
|
||||
checkSetupStatus,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return pipeline(tasks);
|
||||
},
|
||||
|
||||
setup: function setup(object) {
|
||||
var setupUser;
|
||||
/**
|
||||
* Executes the setup tasks and sends an email to the owner
|
||||
* @param {Object} setupDetails
|
||||
* @return {Promise<Object>} a user api payload
|
||||
*/
|
||||
setup: function setup(setupDetails) {
|
||||
var tasks;
|
||||
|
||||
return authentication.isSetup().then(function (result) {
|
||||
var setup = result.setup[0].status;
|
||||
|
||||
if (setup) {
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupAlreadyCompleted')));
|
||||
}
|
||||
|
||||
return setupTasks(object);
|
||||
}).then(function (result) {
|
||||
setupUser = result;
|
||||
function doSetup(setupDetails) {
|
||||
return setupTasks(setupDetails);
|
||||
}
|
||||
|
||||
function sendNotification(setupUser) {
|
||||
var data = {
|
||||
ownerEmail: setupUser.email
|
||||
};
|
||||
|
||||
return mail.generateContent({data: data, template: 'welcome'});
|
||||
}).then(function (emailContent) {
|
||||
var message = {
|
||||
to: setupUser.email,
|
||||
subject: i18n.t('common.api.authentication.mail.yourNewGhostBlog'),
|
||||
html: emailContent.html,
|
||||
text: emailContent.text
|
||||
},
|
||||
payload = {
|
||||
mail: [{
|
||||
message: message,
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
return mail.generateContent({data: data, template: 'welcome'})
|
||||
.then(function then(content) {
|
||||
var message = {
|
||||
to: setupUser.email,
|
||||
subject: i18n.t('common.api.authentication.mail.yourNewGhostBlog'),
|
||||
html: content.html,
|
||||
text: content.text
|
||||
},
|
||||
payload = {
|
||||
mail: [{
|
||||
message: message,
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
|
||||
mail.send(payload, {context: {internal: true}}).catch(function (error) {
|
||||
errors.logError(
|
||||
error.message,
|
||||
i18n.t('errors.api.authentication.unableToSendWelcomeEmail', {url: 'http://support.ghost.org/mail/'}),
|
||||
i18n.t('errors.api.authentication.checkEmailConfigInstructions')
|
||||
);
|
||||
});
|
||||
}).then(function () {
|
||||
return Promise.resolve({users: [setupUser]});
|
||||
});
|
||||
},
|
||||
|
||||
updateSetup: function updateSetup(object, options) {
|
||||
if (!options.context || !options.context.user) {
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.notLoggedIn')));
|
||||
mail.send(payload, {context: {internal: true}}).catch(function (error) {
|
||||
errors.logError(
|
||||
error.message,
|
||||
i18n.t(
|
||||
'errors.api.authentication.unableToSendWelcomeEmail',
|
||||
{url: 'http://support.ghost.org/mail/'}
|
||||
),
|
||||
i18n.t('errors.api.authentication.checkEmailConfigInstructions')
|
||||
);
|
||||
});
|
||||
})
|
||||
.return(setupUser);
|
||||
}
|
||||
|
||||
return dataProvider.User.findOne({role: 'Owner', status: 'all'}).then(function (result) {
|
||||
var user = result.toJSON();
|
||||
function formatResponse(setupUser) {
|
||||
return {users: [setupUser]};
|
||||
}
|
||||
|
||||
if (user.id !== options.context.user) {
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner')));
|
||||
tasks = [
|
||||
assertSetupCompleted(false),
|
||||
doSetup,
|
||||
sendNotification,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return pipeline(tasks, setupDetails);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the blog setup
|
||||
* @param {Object} setupDetails request payload with setup details
|
||||
* @param {Object} options
|
||||
* @return {Promise<Object>} a User API response payload
|
||||
*/
|
||||
updateSetup: function updateSetup(setupDetails, options) {
|
||||
var tasks,
|
||||
localOptions = _.cloneDeep(options || {});
|
||||
|
||||
function processArgs(setupDetails, options) {
|
||||
if (!options.context || !options.context.user) {
|
||||
throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner'));
|
||||
}
|
||||
|
||||
return setupTasks(object);
|
||||
}).then(function (result) {
|
||||
return Promise.resolve({users: [result]});
|
||||
});
|
||||
},
|
||||
|
||||
revoke: function (object) {
|
||||
var token;
|
||||
|
||||
if (object.token_type_hint && object.token_type_hint === 'access_token') {
|
||||
token = dataProvider.Accesstoken;
|
||||
} else if (object.token_type_hint && object.token_type_hint === 'refresh_token') {
|
||||
token = dataProvider.Refreshtoken;
|
||||
} else {
|
||||
return errors.BadRequestError(i18n.t('errors.api.authentication.invalidTokenTypeHint'));
|
||||
return _.assign({setupDetails: setupDetails}, options);
|
||||
}
|
||||
|
||||
return token.destroyByToken({token: object.token}).then(function () {
|
||||
return Promise.resolve({token: object.token});
|
||||
}, function () {
|
||||
// On error we still want a 200. See https://tools.ietf.org/html/rfc7009#page-5
|
||||
return Promise.resolve({token: object.token, error: i18n.t('errors.api.authentication.invalidTokenProvided')});
|
||||
});
|
||||
function checkPermission(options) {
|
||||
return dataProvider.User.findOne({role: 'Owner', status: 'all'})
|
||||
.then(function (owner) {
|
||||
if (owner.id !== options.context.user) {
|
||||
throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner'));
|
||||
}
|
||||
|
||||
return options.setupDetails;
|
||||
});
|
||||
}
|
||||
|
||||
function formatResponse(user) {
|
||||
return {users: [user]};
|
||||
}
|
||||
|
||||
tasks = [
|
||||
processArgs,
|
||||
assertSetupCompleted(true),
|
||||
checkPermission,
|
||||
setupTasks,
|
||||
formatResponse
|
||||
];
|
||||
|
||||
return pipeline(tasks, setupDetails, localOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Revokes a bearer token.
|
||||
* @param {Object} tokenDetails
|
||||
* @param {Object} options
|
||||
* @return {Promise<Object>} an object containing the revoked token.
|
||||
*/
|
||||
revoke: function revokeToken(tokenDetails, options) {
|
||||
var tasks,
|
||||
localOptions = _.cloneDeep(options || {});
|
||||
|
||||
function processArgs(tokenDetails, options) {
|
||||
return _.assign({}, tokenDetails, options);
|
||||
}
|
||||
|
||||
function revokeToken(options) {
|
||||
var providers = [
|
||||
dataProvider.Refreshtoken,
|
||||
dataProvider.Accesstoken
|
||||
],
|
||||
response = {token: options.token};
|
||||
|
||||
function destroyToken(provider, options, providers) {
|
||||
return provider.destroyByToken(options)
|
||||
.return(response)
|
||||
.catch(provider.NotFoundError, function () {
|
||||
if (!providers.length) {
|
||||
return {
|
||||
token: tokenDetails.token,
|
||||
error: i18n.t('errors.api.authentication.invalidTokenProvided')
|
||||
};
|
||||
}
|
||||
|
||||
return destroyToken(providers.pop(), options, providers);
|
||||
})
|
||||
.catch(function () {
|
||||
throw new errors.TokenRevocationError(
|
||||
i18n.t('errors.api.authentication.tokenRevocationFailed')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return destroyToken(providers.pop(), options, providers);
|
||||
}
|
||||
|
||||
tasks = [
|
||||
processArgs,
|
||||
revokeToken
|
||||
];
|
||||
|
||||
return pipeline(tasks, tokenDetails, localOptions);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -17,6 +17,7 @@ var _ = require('lodash'),
|
||||
EmailError = require('./email-error'),
|
||||
DataImportError = require('./data-import-error'),
|
||||
TooManyRequestsError = require('./too-many-requests-error'),
|
||||
TokenRevocationError = require('./token-revocation-error'),
|
||||
i18n = require('../i18n'),
|
||||
config,
|
||||
errors,
|
||||
@ -431,3 +432,4 @@ module.exports.EmailError = EmailError;
|
||||
module.exports.DataImportError = DataImportError;
|
||||
module.exports.MethodNotAllowedError = MethodNotAllowedError;
|
||||
module.exports.TooManyRequestsError = TooManyRequestsError;
|
||||
module.exports.TokenRevocationError = TokenRevocationError;
|
||||
|
14
core/server/errors/token-revocation-error.js
Normal file
14
core/server/errors/token-revocation-error.js
Normal file
@ -0,0 +1,14 @@
|
||||
// # Token Revocation ERror
|
||||
// Custom error class with status code and type prefilled.
|
||||
|
||||
function TokenRevocationError(message) {
|
||||
this.message = message;
|
||||
this.stack = new Error().stack;
|
||||
this.statusCode = 503;
|
||||
this.errorType = this.name;
|
||||
}
|
||||
|
||||
TokenRevocationError.prototype = Object.create(Error.prototype);
|
||||
TokenRevocationError.prototype.name = 'TokenRevocationError';
|
||||
|
||||
module.exports = TokenRevocationError;
|
@ -68,17 +68,14 @@ Basetoken = ghostBookshelf.Model.extend({
|
||||
var token = options.token;
|
||||
|
||||
options = this.filterOptions(options, 'destroyByUser');
|
||||
options.require = true;
|
||||
|
||||
if (token) {
|
||||
return ghostBookshelf.Collection.forge([], {model: this})
|
||||
.query('where', 'token', '=', token)
|
||||
.fetch(options)
|
||||
.then(function then(collection) {
|
||||
collection.invokeThen('destroy', options);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.base.token.tokenNotFound')));
|
||||
return this.forge()
|
||||
.query('where', 'token', '=', token)
|
||||
.fetch(options)
|
||||
.then(function then(model) {
|
||||
return model.destroy(options);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -278,6 +278,7 @@
|
||||
},
|
||||
"api": {
|
||||
"authentication": {
|
||||
"setupUnableToRun": "Database missing fixture data. Please reset database and try again.",
|
||||
"setupMustBeCompleted": "Setup must be completed before making this request.",
|
||||
"noEmailProvided": "No email provided.",
|
||||
"invalidEmailReceived": "The server did not receive a valid email",
|
||||
@ -287,7 +288,8 @@
|
||||
"notLoggedIn": "You are not logged in.",
|
||||
"notTheBlogOwner": "You are not the blog owner.",
|
||||
"invalidTokenTypeHint": "Invalid token_type_hint given.",
|
||||
"invalidTokenProvided": "Invalid token provided"
|
||||
"invalidTokenProvided": "Invalid token provided",
|
||||
"tokenRevocationFailed": "Token revocation failed"
|
||||
},
|
||||
"clients": {
|
||||
"clientNotFound": "Client not found."
|
||||
|
@ -3,6 +3,10 @@ var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
Promise = require('bluebird'),
|
||||
uid = require('../../../server/utils').uid,
|
||||
Accesstoken,
|
||||
Refreshtoken,
|
||||
User,
|
||||
|
||||
// Stuff we are testing
|
||||
|
||||
@ -51,6 +55,37 @@ describe('Authentication API', function () {
|
||||
should.exist(AuthAPI);
|
||||
|
||||
describe('Setup', function () {
|
||||
describe('Cannot run', function () {
|
||||
before(function () {
|
||||
User = require('../../../server/models/user').User;
|
||||
});
|
||||
|
||||
beforeEach(testUtils.setup('roles', 'owner:pre', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
|
||||
|
||||
describe('Invalid database state', function () {
|
||||
it('should not allow setup to be run if owner missing from database', function (done) {
|
||||
var setupData = {
|
||||
name: 'test user',
|
||||
email: 'test@example.com',
|
||||
password: 'areallygoodpassword',
|
||||
blogTitle: 'a test blog'
|
||||
};
|
||||
|
||||
User.fetchAll().call('invokeThen', 'destroy').then(function () {
|
||||
AuthAPI.setup({setup: [setupData]}).then(function () {
|
||||
done(new Error('Setup ran when it should not have.'));
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
err.name.should.equal('InternalServerError');
|
||||
err.statusCode.should.equal(500);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Not completed', function () {
|
||||
// TODO: stub settings
|
||||
beforeEach(testUtils.setup('roles', 'owner:pre', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
|
||||
@ -89,6 +124,43 @@ describe('Authentication API', function () {
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should allow setup to be completed without a blog title', function (done) {
|
||||
var setupData = {
|
||||
name: 'test user',
|
||||
email: 'test@example.com',
|
||||
password: 'areallygoodpassword'
|
||||
};
|
||||
|
||||
AuthAPI.setup({setup: [setupData]}).then(function (result) {
|
||||
should.exist(result);
|
||||
should.exist(result.users);
|
||||
should.not.exist(result.meta);
|
||||
result.users.should.have.length(1);
|
||||
testUtils.API.checkResponse(result.users[0], 'user');
|
||||
|
||||
var newUser = result.users[0];
|
||||
|
||||
newUser.id.should.equal(1);
|
||||
newUser.name.should.equal(setupData.name);
|
||||
newUser.email.should.equal(setupData.email);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should return an error for an invitation check', function (done) {
|
||||
AuthAPI.isInvitation({email: 'a@example.com'}).then(function () {
|
||||
done(new Error('Did not receive an error response'));
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
|
||||
err.name.should.equal('NoPermissionError');
|
||||
err.statusCode.should.equal(403);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should not allow an invitation to be accepted', function (done) {
|
||||
AuthAPI.acceptInvitation(testInvite).then(function () {
|
||||
done(new Error('Invitation was allowed to be accepted'));
|
||||
@ -99,7 +171,7 @@ describe('Authentication API', function () {
|
||||
err.statusCode.should.equal(403);
|
||||
|
||||
done();
|
||||
});
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should not generate a password reset token', function (done) {
|
||||
@ -112,7 +184,7 @@ describe('Authentication API', function () {
|
||||
err.statusCode.should.equal(403);
|
||||
|
||||
done();
|
||||
});
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should not allow a password reset', function (done) {
|
||||
@ -125,12 +197,18 @@ describe('Authentication API', function () {
|
||||
err.statusCode.should.equal(403);
|
||||
|
||||
done();
|
||||
});
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Completed', function () {
|
||||
beforeEach(testUtils.setup('roles', 'owner', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
|
||||
before(function () {
|
||||
Accesstoken = require('../../../server/models/accesstoken').Accesstoken;
|
||||
Refreshtoken = require('../../../server/models/refreshtoken').Refreshtoken;
|
||||
User = require('../../../server/models/user').User;
|
||||
});
|
||||
|
||||
beforeEach(testUtils.setup('roles', 'owner', 'clients', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
|
||||
|
||||
it('should report that setup has been completed', function (done) {
|
||||
AuthAPI.isSetup().then(function (result) {
|
||||
@ -158,7 +236,7 @@ describe('Authentication API', function () {
|
||||
err.statusCode.should.equal(403);
|
||||
|
||||
done();
|
||||
});
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should allow an invitation to be accepted, but fail on token validation', function (done) {
|
||||
@ -170,8 +248,9 @@ describe('Authentication API', function () {
|
||||
err.name.should.equal('UnauthorizedError');
|
||||
err.statusCode.should.equal(401);
|
||||
err.message.should.equal('Invalid token structure');
|
||||
|
||||
done();
|
||||
});
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should generate a password reset token', function (done) {
|
||||
@ -183,6 +262,23 @@ describe('Authentication API', function () {
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should not generate a password reset token for an invalid email address', function (done) {
|
||||
var badResetRequest = {
|
||||
passwordreset: [{email: ''}]
|
||||
};
|
||||
|
||||
AuthAPI.generateResetToken(badResetRequest).then(function () {
|
||||
done(new Error('reset token was generated for invalid email address'));
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
|
||||
err.name.should.equal('BadRequestError');
|
||||
err.statusCode.should.equal(400);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should allow a password reset', function (done) {
|
||||
AuthAPI.resetPassword(testReset).then(function () {
|
||||
done(new Error('password reset did not fail on token validation'));
|
||||
@ -192,8 +288,141 @@ describe('Authentication API', function () {
|
||||
err.name.should.equal('UnauthorizedError');
|
||||
err.statusCode.should.equal(401);
|
||||
err.message.should.equal('Invalid token structure');
|
||||
|
||||
done();
|
||||
});
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should allow an access token to be revoked', function (done) {
|
||||
var id = uid(256);
|
||||
|
||||
Accesstoken.add({
|
||||
token: id,
|
||||
expires: Date.now() + 8640000,
|
||||
user_id: 1,
|
||||
client_id: 1
|
||||
}).then(function (token) {
|
||||
should.exist(token);
|
||||
token.get('token').should.equal(id);
|
||||
|
||||
return AuthAPI.revoke({
|
||||
token: token.get('token'),
|
||||
token_type_hint: 'access_token'
|
||||
});
|
||||
}).then(function (response) {
|
||||
should.exist(response);
|
||||
response.token.should.equal(id);
|
||||
|
||||
return Accesstoken.findOne({token: id});
|
||||
}).then(function (token) {
|
||||
should.not.exist(token);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should know an email address has an active invitation', function (done) {
|
||||
var user = {
|
||||
name: 'test user',
|
||||
email: 'invited@example.com',
|
||||
password: '12345678',
|
||||
status: 'invited'
|
||||
},
|
||||
options = {
|
||||
context: {internal: true}
|
||||
};
|
||||
|
||||
User.add(user, options).then(function (user) {
|
||||
return AuthAPI.isInvitation({email: user.get('email')});
|
||||
}).then(function (response) {
|
||||
should.exist(response);
|
||||
response.invitation[0].valid.should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should know an email address does not have an active invitation', function (done) {
|
||||
var user = {
|
||||
name: 'uninvited user',
|
||||
email: 'notinvited@example.com',
|
||||
password: '12345678',
|
||||
status: 'active'
|
||||
},
|
||||
options = {
|
||||
context: {internal: true}
|
||||
};
|
||||
|
||||
User.add(user, options).then(function (user) {
|
||||
return AuthAPI.isInvitation({email: user.get('email')});
|
||||
}).then(function (response) {
|
||||
should.exist(response);
|
||||
response.invitation[0].valid.should.be.false();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should know an unknown email address is not an active invitation', function (done) {
|
||||
AuthAPI.isInvitation({email: 'unknown@example.com'}).then(function (response) {
|
||||
should.exist(response);
|
||||
response.invitation[0].valid.should.be.false();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should allow a refresh token to be revoked', function (done) {
|
||||
var id = uid(256);
|
||||
|
||||
Refreshtoken.add({
|
||||
token: id,
|
||||
expires: Date.now() + 8640000,
|
||||
user_id: 1,
|
||||
client_id: 1
|
||||
}).then(function (token) {
|
||||
should.exist(token);
|
||||
token.get('token').should.equal(id);
|
||||
|
||||
return AuthAPI.revoke({
|
||||
token: token.get('token'),
|
||||
token_type_hint: 'refresh_token'
|
||||
});
|
||||
}).then(function (response) {
|
||||
should.exist(response);
|
||||
response.token.should.equal(id);
|
||||
|
||||
return Refreshtoken.findOne({token: id});
|
||||
}).then(function (token) {
|
||||
should.not.exist(token);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should return success when attempting to revoke an invalid token', function (done) {
|
||||
var id = uid(256);
|
||||
|
||||
Accesstoken.add({
|
||||
token: id,
|
||||
expires: Date.now() + 8640000,
|
||||
user_id: 1,
|
||||
client_id: 1
|
||||
}).then(function (token) {
|
||||
should.exist(token);
|
||||
token.get('token').should.equal(id);
|
||||
|
||||
return AuthAPI.revoke({
|
||||
token: 'notavalidtoken',
|
||||
token_type_hint: 'access_token'
|
||||
});
|
||||
}).then(function (response) {
|
||||
should.exist(response);
|
||||
response.token.should.equal('notavalidtoken');
|
||||
response.error.should.equal('Invalid token provided');
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -228,7 +457,7 @@ describe('Authentication API', function () {
|
||||
err.statusCode.should.equal(403);
|
||||
|
||||
done();
|
||||
});
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
@ -261,7 +490,7 @@ describe('Authentication API', function () {
|
||||
err.statusCode.should.equal(403);
|
||||
|
||||
done();
|
||||
});
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user