mirror of
https://github.com/flarum/core.git
synced 2025-07-21 00:31:17 +02:00
Improve client XHR error handling
The default XHR error handler produce an alert which is appropriate to the response status code. It can be overridden per-request (by specifying the `errorHandler` option) so that the alert can be suppressed or displayed in a different position (e.g. inside a modal). ref #118
This commit is contained in:
@@ -85,13 +85,12 @@ export default class EditGroupModal extends Modal {
|
|||||||
namePlural: this.namePlural(),
|
namePlural: this.namePlural(),
|
||||||
color: this.color(),
|
color: this.color(),
|
||||||
icon: this.icon()
|
icon: this.icon()
|
||||||
}).then(
|
}, {errorHandler: this.onerror.bind(this)})
|
||||||
() => this.hide(),
|
.then(this.hide.bind(this))
|
||||||
(response) => {
|
.catch(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.handleErrors(response);
|
m.redraw();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteGroup() {
|
deleteGroup() {
|
||||||
|
@@ -68,11 +68,8 @@ export default class SettingsModal extends Modal {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
saveSettings(this.dirty()).then(
|
saveSettings(this.dirty()).then(
|
||||||
() => this.hide(),
|
this.hide.bind(this),
|
||||||
() => {
|
this.loaded.bind(this)
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -163,5 +163,6 @@ export default class AvatarEditor extends Component {
|
|||||||
*/
|
*/
|
||||||
failure() {
|
failure() {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -83,16 +83,8 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
app.session.user.save({email: this.email()}).then(
|
app.session.user.save({email: this.email()}, {errorHandler: this.onerror.bind(this)})
|
||||||
() => {
|
.then(() => this.success = true)
|
||||||
this.loading = false;
|
.finally(this.loaded.bind(this));
|
||||||
this.success = true;
|
|
||||||
m.redraw();
|
|
||||||
},
|
|
||||||
response => {
|
|
||||||
this.loading = false;
|
|
||||||
this.handleErrors(response);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -42,8 +42,8 @@ export default class ChangePasswordModal extends Modal {
|
|||||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||||
data: {email: app.session.user.email()}
|
data: {email: app.session.user.email()}
|
||||||
}).then(
|
}).then(
|
||||||
() => this.hide(),
|
this.hide.bind(this),
|
||||||
() => this.loading = false
|
this.loaded.bind(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -102,4 +102,12 @@ export default class ComposerBody extends Component {
|
|||||||
*/
|
*/
|
||||||
onsubmit() {
|
onsubmit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop loading.
|
||||||
|
*/
|
||||||
|
loaded() {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -109,11 +109,7 @@ export default class DiscussionComposer extends ComposerBody {
|
|||||||
app.cache.discussionList.addDiscussion(discussion);
|
app.cache.discussionList.addDiscussion(discussion);
|
||||||
m.route(app.route.discussion(discussion));
|
m.route(app.route.discussion(discussion));
|
||||||
},
|
},
|
||||||
response => {
|
this.loaded.bind(this)
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
app.alertErrors(response.errors);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -128,7 +128,7 @@ export default class DiscussionPage extends Page {
|
|||||||
// component for the first time on page load, then any calls to m.redraw
|
// component for the first time on page load, then any calls to m.redraw
|
||||||
// will be ineffective and thus any configs (scroll code) will be run
|
// will be ineffective and thus any configs (scroll code) will be run
|
||||||
// before stuff is drawn to the page.
|
// before stuff is drawn to the page.
|
||||||
setTimeout(this.show.bind(this, preloadedDiscussion));
|
setTimeout(this.show.bind(this, preloadedDiscussion), 0);
|
||||||
} else {
|
} else {
|
||||||
const params = this.requestParams();
|
const params = this.requestParams();
|
||||||
|
|
||||||
|
@@ -64,14 +64,8 @@ export default class EditPostComposer extends ComposerBody {
|
|||||||
const data = this.data();
|
const data = this.data();
|
||||||
|
|
||||||
this.props.post.save(data).then(
|
this.props.post.save(data).then(
|
||||||
() => {
|
() => app.composer.hide(),
|
||||||
app.composer.hide();
|
this.loaded.bind(this)
|
||||||
m.redraw();
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -102,11 +102,7 @@ export default class EditUserModal extends Modal {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onsubmit(e) {
|
data() {
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
const groups = Object.keys(this.groups)
|
const groups = Object.keys(this.groups)
|
||||||
.filter(id => this.groups[id]())
|
.filter(id => this.groups[id]())
|
||||||
.map(id => app.store.getById('groups', id));
|
.map(id => app.store.getById('groups', id));
|
||||||
@@ -121,12 +117,19 @@ export default class EditUserModal extends Modal {
|
|||||||
data.password = this.password();
|
data.password = this.password();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.user.save(data).then(
|
return data;
|
||||||
() => this.hide(),
|
}
|
||||||
response => {
|
|
||||||
|
onsubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.props.user.save(this.data(), {errorHandler: this.onerror.bind(this)})
|
||||||
|
.then(this.hide.bind(this))
|
||||||
|
.catch(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.handleErrors(response);
|
m.redraw();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -85,24 +85,12 @@ export default class ForgotPasswordModal extends Modal {
|
|||||||
app.request({
|
app.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||||
data: {email: this.email()},
|
data: {email: this.email()}
|
||||||
handlers: {
|
})
|
||||||
404: () => {
|
.then(() => {
|
||||||
this.alert = new Alert({type: 'warning', message: 'That email wasn\'t found in our database.'});
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).then(
|
|
||||||
() => {
|
|
||||||
this.loading = false;
|
|
||||||
this.success = true;
|
this.success = true;
|
||||||
this.alert = null;
|
this.alert = null;
|
||||||
m.redraw();
|
})
|
||||||
},
|
.finally(this.loaded.bind(this));
|
||||||
response => {
|
|
||||||
this.loading = false;
|
|
||||||
this.handleErrors(response);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -124,25 +124,25 @@ export default class LogInModal extends Modal {
|
|||||||
const email = this.email();
|
const email = this.email();
|
||||||
const password = this.password();
|
const password = this.password();
|
||||||
|
|
||||||
app.session.login(email, password).then(
|
app.session.login(email, password, {errorHandler: this.onerror.bind(this)})
|
||||||
null,
|
.catch(this.loaded.bind(this));
|
||||||
response => {
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
if (response && response.code === 'confirm_email') {
|
|
||||||
this.alert = Alert.component({
|
|
||||||
children: app.trans('core.forum.log_in_confirmation_required_message', {email: response.email})
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.alert = Alert.component({
|
|
||||||
type: 'error',
|
|
||||||
children: app.trans('core.forum.log_in_invalid_login_message')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.redraw();
|
onerror(error) {
|
||||||
this.onready();
|
switch (error.status) {
|
||||||
|
case 401:
|
||||||
|
error.alert.props.children = app.trans('core.forum.log_in_confirmation_required_message', {email: error.response.emailConfirmationRequired});
|
||||||
|
delete error.alert.props.type;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 404:
|
||||||
|
error.alert.props.children = app.trans('core.forum.log_in_invalid_login_message');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// no default
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
super.onerror(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -115,13 +115,12 @@ export default class NotificationList extends Component {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
|
||||||
app.store.find('notifications').then(notifications => {
|
app.store.find('notifications')
|
||||||
|
.then(notifications => {
|
||||||
app.session.user.pushAttributes({newNotificationsCount: 0});
|
app.session.user.pushAttributes({newNotificationsCount: 0});
|
||||||
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
|
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
|
||||||
|
})
|
||||||
this.loading = false;
|
.finally(this.loaded.bind(this));
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -36,7 +36,8 @@ export default class ReplyComposer extends ComposerBody {
|
|||||||
|
|
||||||
items.add('title', (
|
items.add('title', (
|
||||||
<h3>
|
<h3>
|
||||||
{icon('reply')}{' '}<a href={app.route.discussion(discussion)} config={m.route}>{discussion.title()}</a>
|
{icon('reply')} {' '}
|
||||||
|
<a href={app.route.discussion(discussion)} config={m.route}>{discussion.title()}</a>
|
||||||
</h3>
|
</h3>
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -93,11 +94,7 @@ export default class ReplyComposer extends ComposerBody {
|
|||||||
|
|
||||||
app.composer.hide();
|
app.composer.hide();
|
||||||
},
|
},
|
||||||
response => {
|
this.loaded.bind(this)
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
app.alertErrors(response.errors);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -178,7 +178,8 @@ export default class SignUpModal extends Modal {
|
|||||||
app.request({
|
app.request({
|
||||||
url: app.forum.attribute('baseUrl') + '/register',
|
url: app.forum.attribute('baseUrl') + '/register',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data
|
data,
|
||||||
|
errorHandler: this.onerror.bind(this)
|
||||||
}).then(
|
}).then(
|
||||||
payload => {
|
payload => {
|
||||||
const user = app.store.pushPayload(payload);
|
const user = app.store.pushPayload(payload);
|
||||||
@@ -190,14 +191,10 @@ export default class SignUpModal extends Modal {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
this.welcomeUser = user;
|
this.welcomeUser = user;
|
||||||
this.loading = false;
|
this.loaded();
|
||||||
m.redraw();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
response => {
|
this.loaded.bind(this)
|
||||||
this.loading = false;
|
|
||||||
this.handleErrors(response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -91,10 +91,7 @@ export default class UserBio extends Component {
|
|||||||
if (user.bio() !== value) {
|
if (user.bio() !== value) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
user.save({bio: value}).then(() => {
|
user.save({bio: value}).finally(this.loaded.bind(this));
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
|
@@ -206,10 +206,14 @@ export default class App {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(responseText);
|
return JSON.parse(responseText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new RequestError(e.message, responseText);
|
throw new RequestError(500, responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
options.errorHandler = options.errorHandler || (error => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
// When extracting the data from the response, we can check the server
|
// When extracting the data from the response, we can check the server
|
||||||
// response code and show an error message to the user if something's gone
|
// response code and show an error message to the user if something's gone
|
||||||
// awry.
|
// awry.
|
||||||
@@ -225,26 +229,56 @@ export default class App {
|
|||||||
|
|
||||||
const status = xhr.status;
|
const status = xhr.status;
|
||||||
|
|
||||||
if (status >= 500 && status <= 599) {
|
if (status < 200 || status > 299) {
|
||||||
throw new RequestError('Internal Server Error', responseText);
|
throw new RequestError(status, responseText, xhr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseText;
|
return responseText;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.alerts.dismiss(this.requestErrorAlert);
|
if (this.requestError) this.requestError.hideAlert();
|
||||||
|
|
||||||
// Now make the request. If it's a failure, inspect the error that was
|
// Now make the request. If it's a failure, inspect the error that was
|
||||||
// returned and show an alert containing its contents.
|
// returned and show an alert containing its contents.
|
||||||
return m.request(options).then(null, error => {
|
return m.request(options).then(null, error => {
|
||||||
if (error instanceof RequestError) {
|
this.requestError = error;
|
||||||
this.alerts.show(this.requestErrorAlert = new Alert({
|
|
||||||
|
let children;
|
||||||
|
|
||||||
|
switch (error.status) {
|
||||||
|
case 422:
|
||||||
|
children = error.response.errors
|
||||||
|
.map(error => [error.detail, <br/>])
|
||||||
|
.reduce((a, b) => a.concat(b), [])
|
||||||
|
.slice(0, -1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 401:
|
||||||
|
case 403:
|
||||||
|
children = 'You do not have permission to do that.';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 404:
|
||||||
|
case 410:
|
||||||
|
children = 'The requested resource was not found.';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
children = 'Oops! Something went wrong. Please reload the page and try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
error.alert = new Alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
children: 'Oops! Something went wrong. Please reload the page and try again.',
|
children,
|
||||||
controls: app.forum.attribute('debug') ? [
|
controls: app.forum.attribute('debug') ? [
|
||||||
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>Debug</Button>
|
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>Debug</Button>
|
||||||
] : undefined
|
] : undefined
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
options.errorHandler(error);
|
||||||
|
} catch (error) {
|
||||||
|
this.alerts.show(error.alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
@@ -264,17 +298,19 @@ export default class App {
|
|||||||
/**
|
/**
|
||||||
* Show alert error messages for each error returned in an API response.
|
* Show alert error messages for each error returned in an API response.
|
||||||
*
|
*
|
||||||
* @param {Array} errors
|
* @param {Object} response
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
alertErrors(errors) {
|
alertErrors(response) {
|
||||||
errors.forEach(error => {
|
if (response.errors) {
|
||||||
|
response.errors.forEach(error => {
|
||||||
this.alerts.show(new Alert({
|
this.alerts.show(new Alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
children: error.detail
|
children: error.detail
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a URL to the route with the given name.
|
* Construct a URL to the route with the given name.
|
||||||
|
@@ -117,10 +117,11 @@ export default class Model {
|
|||||||
*
|
*
|
||||||
* @param {Object} attributes The attributes to save. If a 'relationships' key
|
* @param {Object} attributes The attributes to save. If a 'relationships' key
|
||||||
* exists, it will be extracted and relationships will also be saved.
|
* exists, it will be extracted and relationships will also be saved.
|
||||||
|
* @param {Object} [options]
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
save(attributes) {
|
save(attributes, options = {}) {
|
||||||
const data = {
|
const data = {
|
||||||
type: this.data.type,
|
type: this.data.type,
|
||||||
id: this.data.id,
|
id: this.data.id,
|
||||||
@@ -153,11 +154,11 @@ export default class Model {
|
|||||||
|
|
||||||
this.pushData(data);
|
this.pushData(data);
|
||||||
|
|
||||||
return app.request({
|
return app.request(Object.assign({
|
||||||
method: this.exists ? 'PATCH' : 'POST',
|
method: this.exists ? 'PATCH' : 'POST',
|
||||||
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
||||||
data: {data}
|
data: {data}
|
||||||
}).then(
|
}, options)).then(
|
||||||
// If everything went well, we'll make sure the store knows that this
|
// If everything went well, we'll make sure the store knows that this
|
||||||
// model exists now (if it didn't already), and we'll push the data that
|
// model exists now (if it didn't already), and we'll push the data that
|
||||||
// the API returned into the store.
|
// the API returned into the store.
|
||||||
@@ -181,17 +182,18 @@ export default class Model {
|
|||||||
* Send a request to delete the resource.
|
* Send a request to delete the resource.
|
||||||
*
|
*
|
||||||
* @param {Object} data Data to send along with the DELETE request.
|
* @param {Object} data Data to send along with the DELETE request.
|
||||||
|
* @param {Object} [options]
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
delete(data) {
|
delete(data, options = {}) {
|
||||||
if (!this.exists) return m.deferred.resolve().promise;
|
if (!this.exists) return m.deferred.resolve().promise;
|
||||||
|
|
||||||
return app.request({
|
return app.request(Object.assign({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
||||||
data
|
data
|
||||||
}).then(() => {
|
}, options)).then(() => {
|
||||||
this.exists = false;
|
this.exists = false;
|
||||||
this.store.remove(this);
|
this.store.remove(this);
|
||||||
});
|
});
|
||||||
|
@@ -26,15 +26,16 @@ export default class Session {
|
|||||||
*
|
*
|
||||||
* @param {String} identification The username/email.
|
* @param {String} identification The username/email.
|
||||||
* @param {String} password
|
* @param {String} password
|
||||||
|
* @param {Object} [options]
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
login(identification, password) {
|
login(identification, password, options = {}) {
|
||||||
return app.request({
|
return app.request(Object.assign({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: app.forum.attribute('baseUrl') + '/login',
|
url: app.forum.attribute('baseUrl') + '/login',
|
||||||
data: {identification, password}
|
data: {identification, password}
|
||||||
})
|
}, options))
|
||||||
.then(() => window.location.reload());
|
.then(() => window.location.reload());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -79,10 +79,11 @@ export default class Store {
|
|||||||
* Alternatively, if an object is passed, it will be handled as the
|
* Alternatively, if an object is passed, it will be handled as the
|
||||||
* `query` parameter.
|
* `query` parameter.
|
||||||
* @param {Object} [query]
|
* @param {Object} [query]
|
||||||
|
* @param {Object} [options]
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
find(type, id, query = {}) {
|
find(type, id, query = {}, options = {}) {
|
||||||
let data = query;
|
let data = query;
|
||||||
let url = app.forum.attribute('apiUrl') + '/' + type;
|
let url = app.forum.attribute('apiUrl') + '/' + type;
|
||||||
|
|
||||||
@@ -94,11 +95,11 @@ export default class Store {
|
|||||||
url += '/' + id;
|
url += '/' + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.request({
|
return app.request(Object.assign({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
data
|
data
|
||||||
}).then(this.pushPayload.bind(this));
|
}, options)).then(this.pushPayload.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -109,27 +109,28 @@ export default class Modal extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show an alert describing errors returned from the API, and give focus to
|
* Stop loading.
|
||||||
|
*/
|
||||||
|
loaded() {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an alert describing an error returned from the API, and give focus to
|
||||||
* the first relevant field.
|
* the first relevant field.
|
||||||
*
|
*
|
||||||
* @param {Object} response
|
* @param {RequestError} error
|
||||||
*/
|
*/
|
||||||
handleErrors(response) {
|
onerror(error) {
|
||||||
const errors = response && response.errors;
|
this.alert = error.alert;
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
this.alert = new Alert({
|
|
||||||
type: 'error',
|
|
||||||
children: errors.map((error, k) => [error.detail, k < errors.length - 1 ? m('br') : ''])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
|
||||||
if (errors) {
|
if (error.status === 422 && error.response.errors) {
|
||||||
this.$('form [name=' + errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
|
this.$('form [name=' + error.response.errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
|
||||||
} else {
|
} else {
|
||||||
this.$('form :input:first').select();
|
this.onready();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,10 +5,6 @@ export default class RequestErrorModal extends Modal {
|
|||||||
return 'RequestErrorModal Modal--large';
|
return 'RequestErrorModal Modal--large';
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
|
||||||
return this.props.error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
let responseText;
|
let responseText;
|
||||||
|
|
||||||
|
@@ -1,6 +1,15 @@
|
|||||||
export default class RequestError {
|
export default class RequestError {
|
||||||
constructor(message, responseText) {
|
constructor(status, responseText, xhr) {
|
||||||
this.message = message;
|
this.status = status;
|
||||||
this.responseText = responseText;
|
this.responseText = responseText;
|
||||||
|
this.xhr = xhr;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.response = JSON.parse(responseText);
|
||||||
|
} catch (e) {
|
||||||
|
this.response = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.alert = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,11 +12,11 @@ namespace Flarum\Api\Controller;
|
|||||||
|
|
||||||
use Flarum\Api\Command\GenerateAccessToken;
|
use Flarum\Api\Command\GenerateAccessToken;
|
||||||
use Flarum\Core\Repository\UserRepository;
|
use Flarum\Core\Repository\UserRepository;
|
||||||
use Flarum\Core\Exception\PermissionDeniedException;
|
|
||||||
use Flarum\Event\UserEmailChangeWasRequested;
|
use Flarum\Event\UserEmailChangeWasRequested;
|
||||||
use Flarum\Http\Controller\ControllerInterface;
|
use Flarum\Http\Controller\ControllerInterface;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
|
||||||
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
|
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
|
|
||||||
@@ -62,16 +62,13 @@ class TokenController implements ControllerInterface
|
|||||||
$user = $this->users->findByIdentification($identification);
|
$user = $this->users->findByIdentification($identification);
|
||||||
|
|
||||||
if (! $user || ! $user->checkPassword($password)) {
|
if (! $user || ! $user->checkPassword($password)) {
|
||||||
throw new PermissionDeniedException;
|
throw new ModelNotFoundException;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $user->is_activated) {
|
if (! $user->is_activated) {
|
||||||
$this->events->fire(new UserEmailChangeWasRequested($user, $user->email));
|
$this->events->fire(new UserEmailChangeWasRequested($user, $user->email));
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse(['emailConfirmationRequired' => $user->email], 401);
|
||||||
'code' => 'confirm_email',
|
|
||||||
'email' => $user->email
|
|
||||||
], 401);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = $this->bus->dispatch(
|
$token = $this->bus->dispatch(
|
||||||
|
@@ -63,6 +63,7 @@ class EditUserHandler
|
|||||||
|
|
||||||
$attributes = array_get($data, 'attributes', []);
|
$attributes = array_get($data, 'attributes', []);
|
||||||
$relationships = array_get($data, 'relationships', []);
|
$relationships = array_get($data, 'relationships', []);
|
||||||
|
$validate = [];
|
||||||
|
|
||||||
if (isset($attributes['username'])) {
|
if (isset($attributes['username'])) {
|
||||||
$this->assertPermission($canEdit);
|
$this->assertPermission($canEdit);
|
||||||
@@ -72,6 +73,10 @@ class EditUserHandler
|
|||||||
if (isset($attributes['email'])) {
|
if (isset($attributes['email'])) {
|
||||||
if ($isSelf) {
|
if ($isSelf) {
|
||||||
$user->requestEmailChange($attributes['email']);
|
$user->requestEmailChange($attributes['email']);
|
||||||
|
|
||||||
|
if ($attributes['email'] !== $user->email) {
|
||||||
|
$validate['email'] = $attributes['email'];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->assertPermission($canEdit);
|
$this->assertPermission($canEdit);
|
||||||
$user->changeEmail($attributes['email']);
|
$user->changeEmail($attributes['email']);
|
||||||
@@ -81,6 +86,8 @@ class EditUserHandler
|
|||||||
if (isset($attributes['password'])) {
|
if (isset($attributes['password'])) {
|
||||||
$this->assertPermission($canEdit);
|
$this->assertPermission($canEdit);
|
||||||
$user->changePassword($attributes['password']);
|
$user->changePassword($attributes['password']);
|
||||||
|
|
||||||
|
$validate['password'] = $attributes['password'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($attributes['bio'])) {
|
if (isset($attributes['bio'])) {
|
||||||
@@ -127,7 +134,7 @@ class EditUserHandler
|
|||||||
new UserWillBeSaved($user, $actor, $data)
|
new UserWillBeSaved($user, $actor, $data)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->validator->assertValid(array_merge($user->getDirty(), array_only($attributes, ['password', 'email'])));
|
$this->validator->assertValid(array_merge($user->getDirty(), $validate));
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
|
@@ -55,12 +55,11 @@ class LoginController implements ControllerInterface
|
|||||||
$actor = $request->getAttribute('actor');
|
$actor = $request->getAttribute('actor');
|
||||||
$params = array_only($request->getParsedBody(), ['identification', 'password']);
|
$params = array_only($request->getParsedBody(), ['identification', 'password']);
|
||||||
|
|
||||||
$data = json_decode($this->apiClient->send($controller, $actor, [], $params)->getBody());
|
$response = $this->apiClient->send($controller, $actor, [], $params);
|
||||||
|
|
||||||
|
if ($response->getStatusCode() === 200) {
|
||||||
|
$data = json_decode($response->getBody());
|
||||||
|
|
||||||
// TODO: The client needs to pass through exceptions(?) or the whole
|
|
||||||
// response so we can look at the response code. For now if there isn't
|
|
||||||
// any useful data we just assume it's a 401.
|
|
||||||
if (isset($data->userId)) {
|
|
||||||
// Extend the token's expiry to 2 weeks so that we can set a
|
// Extend the token's expiry to 2 weeks so that we can set a
|
||||||
// remember cookie
|
// remember cookie
|
||||||
AccessToken::where('id', $data->token)->update(['expires_at' => new DateTime('+2 weeks')]);
|
AccessToken::where('id', $data->token)->update(['expires_at' => new DateTime('+2 weeks')]);
|
||||||
@@ -68,11 +67,11 @@ class LoginController implements ControllerInterface
|
|||||||
event(new UserLoggedIn($this->users->findOrFail($data->userId), $data->token));
|
event(new UserLoggedIn($this->users->findOrFail($data->userId), $data->token));
|
||||||
|
|
||||||
return $this->withRememberCookie(
|
return $this->withRememberCookie(
|
||||||
new JsonResponse($data),
|
$response,
|
||||||
$data->token
|
$data->token
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return new EmptyResponse(401);
|
return $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user