Merge branch 'sudo-mode'

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Toby Zerner
2015-12-03 15:12:51 +10:30
68 changed files with 1071 additions and 509 deletions

View File

@ -2,6 +2,7 @@ import ItemList from 'flarum/utils/ItemList';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import RequestErrorModal from 'flarum/components/RequestErrorModal';
import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal';
import Translator from 'flarum/Translator';
import extract from 'flarum/utils/extract';
import patchMithril from 'flarum/utils/patchMithril';
@ -182,14 +183,17 @@ export default class App {
* @return {Promise}
* @public
*/
request(options) {
request(originalOptions) {
const options = Object.assign({}, originalOptions);
// Set some default options if they haven't been overridden. We want to
// authenticate all requests with the session token. We also want all
// requests to run asynchronously in the background, so that they don't
// prevent redraws from occurring.
options.config = options.config || this.session.authorize.bind(this.session);
options.background = options.background || true;
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken));
// If the method is something like PATCH or DELETE, which not all servers
// and clients support, then we'll send it as a POST request with the
// intended method specified in the X-HTTP-Method-Override header.
@ -218,7 +222,7 @@ export default class App {
if (original) {
responseText = original(xhr.responseText);
} else {
responseText = xhr.responseText.length > 0 ? xhr.responseText : null;
responseText = xhr.responseText || null;
}
const status = xhr.status;
@ -227,6 +231,11 @@ export default class App {
throw new RequestError(status, responseText, options, xhr);
}
if (xhr.getResponseHeader) {
const csrfToken = xhr.getResponseHeader('X-CSRF-Token');
if (csrfToken) app.session.csrfToken = csrfToken;
}
try {
return JSON.parse(responseText);
} catch (e) {
@ -238,9 +247,20 @@ export default class App {
// Now make the request. If it's a failure, inspect the error that was
// returned and show an alert containing its contents.
return m.request(options).then(null, error => {
const deferred = m.deferred();
m.request(options).then(response => deferred.resolve(response), error => {
this.requestError = error;
if (error.response && error.response.errors && error.response.errors[0] && error.response.errors[0].code === 'invalid_access_token') {
this.modal.show(new ConfirmPasswordModal({
deferredRequest: originalOptions,
deferred,
error
}));
return;
}
let children;
switch (error.status) {
@ -283,8 +303,10 @@ export default class App {
this.alerts.show(error.alert);
}
throw error;
deferred.reject(error);
});
return deferred.promise;
}
/**

View File

@ -150,7 +150,7 @@ export default class Model {
// Before we update the model's data, we should make a copy of the model's
// old data so that we can revert back to it if something goes awry during
// persistence.
const oldData = JSON.parse(JSON.stringify(this.data));
const oldData = this.copyData();
this.pushData(data);
@ -209,6 +209,10 @@ export default class Model {
return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
}
copyData() {
return JSON.parse(JSON.stringify(this.data));
}
/**
* Generate a function which returns the value of the given attribute.
*

View File

@ -3,7 +3,7 @@
* to the current authenticated user, and provides methods to log in/out.
*/
export default class Session {
constructor(token, user) {
constructor(user, csrfToken) {
/**
* The current authenticated user.
*
@ -13,12 +13,12 @@ export default class Session {
this.user = user;
/**
* The token that was used for authentication.
* The CSRF token.
*
* @type {String|null}
* @public
*/
this.token = token;
this.csrfToken = csrfToken;
}
/**
@ -35,8 +35,7 @@ export default class Session {
method: 'POST',
url: app.forum.attribute('baseUrl') + '/login',
data: {identification, password}
}, options))
.then(() => window.location.reload());
}, options));
}
/**
@ -45,19 +44,6 @@ export default class Session {
* @public
*/
logout() {
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
}
/**
* Apply an authorization header with the current token to the given
* XMLHttpRequest object.
*
* @param {XMLHttpRequest} xhr
* @public
*/
authorize(xhr) {
if (this.token) {
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
}
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
}
}

View File

@ -0,0 +1,73 @@
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
import extractText from 'flarum/utils/extractText';
export default class ConfirmPasswordModal extends Modal {
init() {
super.init();
this.password = m.prop('');
}
className() {
return 'ConfirmPasswordModal Modal--small';
}
title() {
return app.translator.trans('core.forum.confirm_password.title');
}
content() {
return (
<div className="Modal-body">
<div className="Form Form--centered">
<div className="Form-group">
<input
type="password"
className="FormControl"
bidi={this.password}
placeholder={extractText(app.translator.trans('core.forum.confirm_password.password_placeholder'))}
disabled={this.loading}/>
</div>
<div className="Form-group">
<Button
type="submit"
className="Button Button--primary Button--block"
loading={this.loading}>
{app.translator.trans('core.forum.confirm_password.submit_button')}
</Button>
</div>
</div>
</div>
);
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
app.session.login(app.session.user.email(), this.password(), {errorHandler: this.onerror.bind(this)})
.then(() => {
this.success = true;
this.hide();
app.request(this.props.deferredRequest).then(response => this.props.deferred.resolve(response), response => this.props.deferred.reject(response));
})
.catch(this.loaded.bind(this));
}
onerror(error) {
if (error.status === 401) {
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
}
super.onerror(error);
}
onhide() {
if (this.success) return;
this.props.deferred.reject(this.props.error);
}
}

View File

@ -98,7 +98,10 @@ export default class Modal extends Component {
* Focus on the first input when the modal is ready to be used.
*/
onready() {
this.$('form :input:first').focus().select();
this.$('form').find('input, select, textarea').first().focus().select();
}
onhide() {
}
/**

View File

@ -77,6 +77,10 @@ export default class ModalManager extends Component {
* @protected
*/
clear() {
if (this.component) {
this.component.onhide();
}
this.component = null;
m.lazyRedraw();

View File

@ -18,7 +18,7 @@ export default function preload(app) {
app.forum = app.store.getById('forums', 1);
app.session = new Session(
app.preload.session.token,
app.store.getById('users', app.preload.session.userId)
app.store.getById('users', app.preload.session.userId),
app.preload.session.csrfToken
);
}