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:
parent
1751daa9e7
commit
69d020ce44
7 changed files with 49 additions and 20 deletions
|
@ -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?".');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
14
core/server/errors/too-many-requests-error.js
Normal file
14
core/server/errors/too-many-requests-error.js
Normal 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;
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue