0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Fix signin errors

refs #5635

- fixes format for server errors
- changes signin-api validation errors to be text rather than alerts
This commit is contained in:
cobbspur 2015-08-10 12:26:39 +01:00
parent 1751daa9e7
commit 69d020ce44
7 changed files with 49 additions and 20 deletions

View file

@ -7,18 +7,24 @@ export default Ember.Controller.extend(ValidationEngine, {
ghostPaths: Ember.inject.service('ghost-paths'), ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(), notifications: Ember.inject.service(),
flowErrors: '',
// ValidationEngine settings // ValidationEngine settings
validationType: 'signin', validationType: 'signin',
actions: { actions: {
authenticate: function () { authenticate: function () {
var model = this.get('model'), var self = this,
model = this.get('model'),
authStrategy = 'simple-auth-authenticator:oauth2-password-grant', authStrategy = 'simple-auth-authenticator:oauth2-password-grant',
data = model.getProperties('identification', 'password'); data = model.getProperties('identification', 'password');
this.get('session').authenticate(authStrategy, data).catch(function () { this.get('session').authenticate(authStrategy, data).catch(function (err) {
// if authentication fails a rejected promise will be returned. if (err.errors) {
self.set('flowErrors', err.errors[0].message.string);
}
// If authentication fails a rejected promise will be returned.
// it needs to be caught so it doesn't generate an exception in the console, // it needs to be caught so it doesn't generate an exception in the console,
// but it's actually "handled" by the sessionAuthenticationFailed action handler. // but it's actually "handled" by the sessionAuthenticationFailed action handler.
}); });
@ -26,7 +32,7 @@ export default Ember.Controller.extend(ValidationEngine, {
validateAndAuthenticate: function () { validateAndAuthenticate: function () {
var self = this; var self = this;
this.set('flowErrors', '');
// Manually trigger events for input fields, ensuring legacy compatibility with // Manually trigger events for input fields, ensuring legacy compatibility with
// browsers and password managers that don't send proper events on autofill // browsers and password managers that don't send proper events on autofill
$('#login').find('input').trigger('change'); $('#login').find('input').trigger('change');
@ -37,6 +43,8 @@ export default Ember.Controller.extend(ValidationEngine, {
}).catch(function (error) { }).catch(function (error) {
if (error) { if (error) {
self.get('notifications').showAPIError(error); self.get('notifications').showAPIError(error);
} else {
self.set('flowErrors', 'Please fill out the form to sign in.');
} }
}); });
}, },
@ -46,6 +54,7 @@ export default Ember.Controller.extend(ValidationEngine, {
notifications = this.get('notifications'), notifications = this.get('notifications'),
self = this; self = this;
this.set('flowErrors', '');
this.validate({property: 'identification'}).then(function () { this.validate({property: 'identification'}).then(function () {
self.set('submitting', true); self.set('submitting', true);
@ -62,8 +71,14 @@ export default Ember.Controller.extend(ValidationEngine, {
notifications.showAlert('Please check your email for instructions.', {type: 'info'}); notifications.showAlert('Please check your email for instructions.', {type: 'info'});
}).catch(function (resp) { }).catch(function (resp) {
self.set('submitting', false); self.set('submitting', false);
notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.'}); if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) {
self.set('flowErrors', resp.jqXHR.responseJSON.errors[0].message);
} else {
notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.'});
}
}); });
}).catch(function () {
self.set('flowErrors', 'Please enter an email address then click "Forgot?".');
}); });
} }
} }

View file

@ -62,10 +62,8 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
error.errors.forEach(function (err) { error.errors.forEach(function (err) {
err.message = err.message.htmlSafe(); err.message = err.message.htmlSafe();
}); });
this.get('notifications').showErrors(error.errors);
} else { } else {
// connection errors don't return proper status message, only req.body // Connection errors don't return proper status message, only req.body
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error'}); this.get('notifications').showAlert('There was a problem on the server.', {type: 'error'});
} }
}, },

View file

@ -18,7 +18,7 @@
<button id="login-button" class="login btn btn-blue btn-block" type="submit" tabindex="3" disabled={{submitting}}>Sign in</button> <button id="login-button" class="login btn btn-blue btn-block" type="submit" tabindex="3" disabled={{submitting}}>Sign in</button>
</form> </form>
<p class="main-error">The password fairy does not approve</p> <p class="main-error">{{{flowErrors}}}</p>
</section> </section>
</div> </div>
</div> </div>

View file

@ -16,6 +16,7 @@ var _ = require('lodash'),
UnsupportedMediaTypeError = require('./unsupported-media-type-error'), UnsupportedMediaTypeError = require('./unsupported-media-type-error'),
EmailError = require('./email-error'), EmailError = require('./email-error'),
DataImportError = require('./data-import-error'), DataImportError = require('./data-import-error'),
TooManyRequestsError = require('./too-many-requests-error'),
config, config,
errors, errors,
@ -393,3 +394,4 @@ module.exports.UnsupportedMediaTypeError = UnsupportedMediaTypeError;
module.exports.EmailError = EmailError; module.exports.EmailError = EmailError;
module.exports.DataImportError = DataImportError; module.exports.DataImportError = DataImportError;
module.exports.MethodNotAllowedError = MethodNotAllowedError; module.exports.MethodNotAllowedError = MethodNotAllowedError;
module.exports.TooManyRequestsError = TooManyRequestsError;

View file

@ -0,0 +1,14 @@
// # Too Many Requests Error
// Custom error class with status code and type prefilled.
function TooManyRequestsError(message) {
this.message = message;
this.stack = new Error().stack;
this.code = 429;
this.errorType = this.name;
}
TooManyRequestsError.prototype = Object.create(Error.prototype);
TooManyRequestsError.prototype.name = 'TooManyRequestsError';
module.exports = TooManyRequestsError;

View file

@ -49,7 +49,7 @@ spamPrevention = {
'Too many login attempts.' 'Too many login attempts.'
); );
message += rateSigninPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later'; message += rateSigninPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later';
return next(new errors.UnauthorizedError(message)); return next(new errors.TooManyRequestsError(message));
} }
next(); next();
}, },
@ -110,7 +110,7 @@ spamPrevention = {
if (deniedEmailRateLimit || deniedRateLimit) { if (deniedEmailRateLimit || deniedRateLimit) {
message += rateForgottenPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later'; message += rateForgottenPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later';
return next(new errors.UnauthorizedError(message)); return next(new errors.TooManyRequestsError(message));
} }
next(); next();

View file

@ -115,7 +115,7 @@ User = ghostBookshelf.Model.extend({
} else if (this.get('id')) { } else if (this.get('id')) {
return this.get('id'); return this.get('id');
} else { } else {
errors.logAndThrowError(new Error('missing context')); errors.logAndThrowError(new errors.NotFoundError('missing context'));
} }
}, },
@ -541,7 +541,7 @@ User = ghostBookshelf.Model.extend({
if (user.get('status') === 'invited' || user.get('status') === 'invited-pending' || if (user.get('status') === 'invited' || user.get('status') === 'invited-pending' ||
user.get('status') === 'inactive' user.get('status') === 'inactive'
) { ) {
return Promise.reject(new Error('The user with that email address is inactive.')); return Promise.reject(new errors.NoPermissionError('The user with that email address is inactive.'));
} }
if (user.get('status') !== 'locked') { if (user.get('status') !== 'locked') {
return bcryptCompare(object.password, user.get('password')).then(function then(matched) { return bcryptCompare(object.password, user.get('password')).then(function then(matched) {
@ -664,25 +664,25 @@ User = ghostBookshelf.Model.extend({
// Check if invalid structure // Check if invalid structure
if (!parts || parts.length !== 3) { if (!parts || parts.length !== 3) {
return Promise.reject(new Error('Invalid token structure')); return Promise.reject(new errors.BadRequestError('Invalid token structure'));
} }
expires = parseInt(parts[0], 10); expires = parseInt(parts[0], 10);
email = parts[1]; email = parts[1];
if (isNaN(expires)) { if (isNaN(expires)) {
return Promise.reject(new Error('Invalid token expiration')); return Promise.reject(new errors.BadRequestError('Invalid token expiration'));
} }
// Check if token is expired to prevent replay attacks // Check if token is expired to prevent replay attacks
if (expires < Date.now()) { if (expires < Date.now()) {
return Promise.reject(new Error('Expired token')); return Promise.reject(new errors.ValidationError('Expired token'));
} }
// to prevent brute force attempts to reset the password the combination of email+expires is only allowed for // to prevent brute force attempts to reset the password the combination of email+expires is only allowed for
// 10 attempts // 10 attempts
if (tokenSecurity[email + '+' + expires] && tokenSecurity[email + '+' + expires].count >= 10) { if (tokenSecurity[email + '+' + expires] && tokenSecurity[email + '+' + expires].count >= 10) {
return Promise.reject(new Error('Token locked')); return Promise.reject(new errors.NoPermissionError('Token locked'));
} }
return this.generateResetToken(email, expires, dbHash).then(function then(generatedToken) { return this.generateResetToken(email, expires, dbHash).then(function then(generatedToken) {
@ -707,7 +707,7 @@ User = ghostBookshelf.Model.extend({
tokenSecurity[email + '+' + expires] = { tokenSecurity[email + '+' + expires] = {
count: tokenSecurity[email + '+' + expires] ? tokenSecurity[email + '+' + expires].count + 1 : 1 count: tokenSecurity[email + '+' + expires] ? tokenSecurity[email + '+' + expires].count + 1 : 1
}; };
return Promise.reject(new Error('Invalid token')); return Promise.reject(new errors.BadRequestError('Invalid token'));
}); });
}, },
@ -719,7 +719,7 @@ User = ghostBookshelf.Model.extend({
dbHash = options.dbHash; dbHash = options.dbHash;
if (newPassword !== ne2Password) { if (newPassword !== ne2Password) {
return Promise.reject(new Error('Your new passwords do not match')); return Promise.reject(new errors.ValidationError('Your new passwords do not match'));
} }
if (!validatePasswordLength(newPassword)) { if (!validatePasswordLength(newPassword)) {
@ -735,7 +735,7 @@ User = ghostBookshelf.Model.extend({
); );
}).then(function then(results) { }).then(function then(results) {
if (!results[0]) { if (!results[0]) {
return Promise.reject(new Error('User not found')); return Promise.reject(new errors.NotFoundError('User not found'));
} }
// Update the user with the new password hash // Update the user with the new password hash