mirror of
https://github.com/flarum/framework.git
synced 2025-05-21 22:36:01 +08:00
Merge branch 'sudo-mode'
# Conflicts: # CHANGELOG.md
This commit is contained in:
@ -81,10 +81,17 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldEmail = app.session.user.email();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
app.session.user.save({email: this.email()}, {errorHandler: this.onerror.bind(this)})
|
app.session.user.save({email: this.email()}, {errorHandler: this.onerror.bind(this)})
|
||||||
.then(() => this.success = true)
|
.then(() => this.success = true)
|
||||||
.finally(this.loaded.bind(this));
|
.finally(this.loaded.bind(this));
|
||||||
|
|
||||||
|
// The save method will update the cached email address on the user model...
|
||||||
|
// But in the case of a "sudo" password prompt, we'll still want to have
|
||||||
|
// the old email address on file for the purposes of logging in.
|
||||||
|
app.session.user.pushAttributes({email: oldEmail});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,8 +124,10 @@ 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, {errorHandler: this.onerror.bind(this)})
|
app.session.login(email, password, {errorHandler: this.onerror.bind(this)}).then(
|
||||||
.catch(this.loaded.bind(this));
|
() => window.location.reload(),
|
||||||
|
this.loaded.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onerror(error) {
|
onerror(error) {
|
||||||
|
@ -18,6 +18,8 @@ import ItemList from 'flarum/utils/ItemList';
|
|||||||
*/
|
*/
|
||||||
export default class Post extends Component {
|
export default class Post extends Component {
|
||||||
init() {
|
init() {
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up a subtree retainer so that the post will not be redrawn
|
* Set up a subtree retainer so that the post will not be redrawn
|
||||||
* unless new data comes in.
|
* unless new data comes in.
|
||||||
@ -37,7 +39,7 @@ export default class Post extends Component {
|
|||||||
view() {
|
view() {
|
||||||
const attrs = this.attrs();
|
const attrs = this.attrs();
|
||||||
|
|
||||||
attrs.className = 'Post ' + (attrs.className || '');
|
attrs.className = 'Post ' + (this.loading ? 'Post--loading ' : '') + (attrs.className || '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article {...attrs}>
|
<article {...attrs}>
|
||||||
|
@ -217,18 +217,19 @@ export default {
|
|||||||
*/
|
*/
|
||||||
deleteAction() {
|
deleteAction() {
|
||||||
if (confirm(extractText(app.translator.trans('core.forum.discussion_controls.delete_confirmation')))) {
|
if (confirm(extractText(app.translator.trans('core.forum.discussion_controls.delete_confirmation')))) {
|
||||||
// If there is a discussion list in the cache, remove this discussion.
|
|
||||||
if (app.cache.discussionList) {
|
|
||||||
app.cache.discussionList.removeDiscussion(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're currently viewing the discussion that was deleted, go back
|
// If we're currently viewing the discussion that was deleted, go back
|
||||||
// to the previous page.
|
// to the previous page.
|
||||||
if (app.viewingDiscussion(this)) {
|
if (app.viewingDiscussion(this)) {
|
||||||
app.history.back();
|
app.history.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.delete();
|
return this.delete().then(() => {
|
||||||
|
// If there is a discussion list in the cache, remove this discussion.
|
||||||
|
if (app.cache.discussionList) {
|
||||||
|
app.cache.discussionList.removeDiscussion(this);
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export default {
|
|||||||
* @return {ItemList}
|
* @return {ItemList}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
destructiveControls(post) {
|
destructiveControls(post, context) {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
if (post.contentType() === 'comment' && !post.isHidden()) {
|
if (post.contentType() === 'comment' && !post.isHidden()) {
|
||||||
@ -101,7 +101,7 @@ export default {
|
|||||||
items.add('delete', Button.component({
|
items.add('delete', Button.component({
|
||||||
icon: 'times',
|
icon: 'times',
|
||||||
children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
|
children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
|
||||||
onclick: this.deleteAction.bind(post)
|
onclick: this.deleteAction.bind(post, context)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,9 +144,14 @@ export default {
|
|||||||
*
|
*
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
deleteAction() {
|
deleteAction(context) {
|
||||||
this.discussion().removePost(this.id());
|
if (context) context.loading = true;
|
||||||
|
|
||||||
return this.delete();
|
return this.delete().then(() => {
|
||||||
|
this.discussion().removePost(this.id());
|
||||||
|
}).finally(() => {
|
||||||
|
if (context) context.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import ItemList from 'flarum/utils/ItemList';
|
|||||||
import Alert from 'flarum/components/Alert';
|
import Alert from 'flarum/components/Alert';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
import RequestErrorModal from 'flarum/components/RequestErrorModal';
|
import RequestErrorModal from 'flarum/components/RequestErrorModal';
|
||||||
|
import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal';
|
||||||
import Translator from 'flarum/Translator';
|
import Translator from 'flarum/Translator';
|
||||||
import extract from 'flarum/utils/extract';
|
import extract from 'flarum/utils/extract';
|
||||||
import patchMithril from 'flarum/utils/patchMithril';
|
import patchMithril from 'flarum/utils/patchMithril';
|
||||||
@ -182,14 +183,17 @@ export default class App {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
request(options) {
|
request(originalOptions) {
|
||||||
|
const options = Object.assign({}, originalOptions);
|
||||||
|
|
||||||
// Set some default options if they haven't been overridden. We want to
|
// Set some default options if they haven't been overridden. We want to
|
||||||
// authenticate all requests with the session token. We also want all
|
// authenticate all requests with the session token. We also want all
|
||||||
// requests to run asynchronously in the background, so that they don't
|
// requests to run asynchronously in the background, so that they don't
|
||||||
// prevent redraws from occurring.
|
// prevent redraws from occurring.
|
||||||
options.config = options.config || this.session.authorize.bind(this.session);
|
|
||||||
options.background = options.background || true;
|
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
|
// 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
|
// and clients support, then we'll send it as a POST request with the
|
||||||
// intended method specified in the X-HTTP-Method-Override header.
|
// intended method specified in the X-HTTP-Method-Override header.
|
||||||
@ -218,7 +222,7 @@ export default class App {
|
|||||||
if (original) {
|
if (original) {
|
||||||
responseText = original(xhr.responseText);
|
responseText = original(xhr.responseText);
|
||||||
} else {
|
} else {
|
||||||
responseText = xhr.responseText.length > 0 ? xhr.responseText : null;
|
responseText = xhr.responseText || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = xhr.status;
|
const status = xhr.status;
|
||||||
@ -227,6 +231,11 @@ export default class App {
|
|||||||
throw new RequestError(status, responseText, options, xhr);
|
throw new RequestError(status, responseText, options, xhr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (xhr.getResponseHeader) {
|
||||||
|
const csrfToken = xhr.getResponseHeader('X-CSRF-Token');
|
||||||
|
if (csrfToken) app.session.csrfToken = csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(responseText);
|
return JSON.parse(responseText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -238,9 +247,20 @@ export default class App {
|
|||||||
|
|
||||||
// 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 => {
|
const deferred = m.deferred();
|
||||||
|
|
||||||
|
m.request(options).then(response => deferred.resolve(response), error => {
|
||||||
this.requestError = 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;
|
let children;
|
||||||
|
|
||||||
switch (error.status) {
|
switch (error.status) {
|
||||||
@ -283,8 +303,10 @@ export default class App {
|
|||||||
this.alerts.show(error.alert);
|
this.alerts.show(error.alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
deferred.reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -150,7 +150,7 @@ export default class Model {
|
|||||||
// Before we update the model's data, we should make a copy of the model's
|
// 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
|
// old data so that we can revert back to it if something goes awry during
|
||||||
// persistence.
|
// persistence.
|
||||||
const oldData = JSON.parse(JSON.stringify(this.data));
|
const oldData = this.copyData();
|
||||||
|
|
||||||
this.pushData(data);
|
this.pushData(data);
|
||||||
|
|
||||||
@ -209,6 +209,10 @@ export default class Model {
|
|||||||
return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
|
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.
|
* Generate a function which returns the value of the given attribute.
|
||||||
*
|
*
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* to the current authenticated user, and provides methods to log in/out.
|
* to the current authenticated user, and provides methods to log in/out.
|
||||||
*/
|
*/
|
||||||
export default class Session {
|
export default class Session {
|
||||||
constructor(token, user) {
|
constructor(user, csrfToken) {
|
||||||
/**
|
/**
|
||||||
* The current authenticated user.
|
* The current authenticated user.
|
||||||
*
|
*
|
||||||
@ -13,12 +13,12 @@ export default class Session {
|
|||||||
this.user = user;
|
this.user = user;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The token that was used for authentication.
|
* The CSRF token.
|
||||||
*
|
*
|
||||||
* @type {String|null}
|
* @type {String|null}
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
this.token = token;
|
this.csrfToken = csrfToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,8 +35,7 @@ export default class Session {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: app.forum.attribute('baseUrl') + '/login',
|
url: app.forum.attribute('baseUrl') + '/login',
|
||||||
data: {identification, password}
|
data: {identification, password}
|
||||||
}, options))
|
}, options));
|
||||||
.then(() => window.location.reload());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,19 +44,6 @@ export default class Session {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
logout() {
|
logout() {
|
||||||
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
|
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
73
js/lib/components/ConfirmPasswordModal.js
Normal file
73
js/lib/components/ConfirmPasswordModal.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -98,7 +98,10 @@ export default class Modal extends Component {
|
|||||||
* Focus on the first input when the modal is ready to be used.
|
* Focus on the first input when the modal is ready to be used.
|
||||||
*/
|
*/
|
||||||
onready() {
|
onready() {
|
||||||
this.$('form :input:first').focus().select();
|
this.$('form').find('input, select, textarea').first().focus().select();
|
||||||
|
}
|
||||||
|
|
||||||
|
onhide() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +77,10 @@ export default class ModalManager extends Component {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
clear() {
|
clear() {
|
||||||
|
if (this.component) {
|
||||||
|
this.component.onhide();
|
||||||
|
}
|
||||||
|
|
||||||
this.component = null;
|
this.component = null;
|
||||||
|
|
||||||
m.lazyRedraw();
|
m.lazyRedraw();
|
||||||
|
@ -18,7 +18,7 @@ export default function preload(app) {
|
|||||||
app.forum = app.store.getById('forums', 1);
|
app.forum = app.store.getById('forums', 1);
|
||||||
|
|
||||||
app.session = new Session(
|
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -167,6 +167,9 @@
|
|||||||
color: @muted-more-color;
|
color: @muted-more-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.Post--loading {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
.PostMeta {
|
.PostMeta {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
32
migrations/2015_12_03_010529_drop_access_tokens_table.php
Normal file
32
migrations/2015_12_03_010529_drop_access_tokens_table.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Core\Migration;
|
||||||
|
|
||||||
|
use Flarum\Database\AbstractMigration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
|
||||||
|
class DropAccessTokensTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$this->schema->drop('access_tokens');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
$this->schema->create('access_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('id', 100)->primary();
|
||||||
|
$table->integer('user_id')->unsigned();
|
||||||
|
$table->timestamp('created_at');
|
||||||
|
$table->timestamp('expires_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
34
migrations/2015_12_03_010610_create_sessions_table.php
Normal file
34
migrations/2015_12_03_010610_create_sessions_table.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Core\Migration;
|
||||||
|
|
||||||
|
use Flarum\Database\AbstractMigration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
|
||||||
|
class CreateSessionsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$this->schema->create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id', 40)->primary();
|
||||||
|
$table->integer('user_id')->unsigned()->nullable();
|
||||||
|
$table->string('csrf_token', 40);
|
||||||
|
$table->integer('last_activity');
|
||||||
|
$table->integer('duration');
|
||||||
|
$table->dateTime('sudo_expiry_time');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
$this->schema->drop('sessions');
|
||||||
|
}
|
||||||
|
}
|
@ -42,6 +42,8 @@ class AdminServiceProvider extends AbstractServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
|
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin');
|
||||||
|
|
||||||
$this->flushAssetsWhenThemeChanged();
|
$this->flushAssetsWhenThemeChanged();
|
||||||
|
|
||||||
$this->flushAssetsWhenExtensionsChanged();
|
$this->flushAssetsWhenExtensionsChanged();
|
||||||
|
@ -10,26 +10,39 @@
|
|||||||
|
|
||||||
namespace Flarum\Admin\Middleware;
|
namespace Flarum\Admin\Middleware;
|
||||||
|
|
||||||
use Flarum\Core\Access\Gate;
|
use Exception;
|
||||||
|
use Flarum\Core\Access\AssertPermissionTrait;
|
||||||
|
use Flarum\Forum\Controller\LogInController;
|
||||||
use Illuminate\Contracts\Container\Container;
|
use Illuminate\Contracts\Container\Container;
|
||||||
|
use Illuminate\Contracts\View\Factory;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Flarum\Core\Exception\PermissionDeniedException;
|
use Zend\Diactoros\Response\HtmlResponse;
|
||||||
|
use Zend\Diactoros\Response\RedirectResponse;
|
||||||
use Zend\Stratigility\MiddlewareInterface;
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
class RequireAdministrateAbility implements MiddlewareInterface
|
class RequireAdministrateAbility implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
/**
|
use AssertPermissionTrait;
|
||||||
* @var Gate
|
|
||||||
*/
|
|
||||||
protected $gate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Gate $gate
|
* @var LogInController
|
||||||
*/
|
*/
|
||||||
public function __construct(Gate $gate)
|
private $logInController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Factory
|
||||||
|
*/
|
||||||
|
private $view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param LogInController $logInController
|
||||||
|
* @param Factory $view
|
||||||
|
*/
|
||||||
|
public function __construct(LogInController $logInController, Factory $view)
|
||||||
{
|
{
|
||||||
$this->gate = $gate;
|
$this->logInController = $logInController;
|
||||||
|
$this->view = $view;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,10 +50,24 @@ class RequireAdministrateAbility implements MiddlewareInterface
|
|||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
{
|
{
|
||||||
$actor = $request->getAttribute('actor');
|
try {
|
||||||
|
$this->assertAdminAndSudo($request);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($request->getMethod() === 'POST') {
|
||||||
|
$response = $this->logInController->handle($request);
|
||||||
|
|
||||||
if (! $this->gate->forUser($actor)->allows('administrate')) {
|
if ($response->getStatusCode() === 200) {
|
||||||
throw new PermissionDeniedException;
|
return $response
|
||||||
|
->withStatus(302)
|
||||||
|
->withHeader('location', app('Flarum\Admin\UrlGenerator')->toRoute('index'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HtmlResponse(
|
||||||
|
$this->view->make('flarum.admin::login')
|
||||||
|
->with('token', $request->getAttribute('session')->csrf_token)
|
||||||
|
->render()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $out ? $out($request, $response) : $response;
|
return $out ? $out($request, $response) : $response;
|
||||||
|
@ -13,6 +13,7 @@ namespace Flarum\Admin;
|
|||||||
|
|
||||||
use Flarum\Foundation\Application;
|
use Flarum\Foundation\Application;
|
||||||
use Flarum\Http\AbstractServer;
|
use Flarum\Http\AbstractServer;
|
||||||
|
use Zend\Diactoros\Response\HtmlResponse;
|
||||||
use Zend\Stratigility\MiddlewarePipe;
|
use Zend\Stratigility\MiddlewarePipe;
|
||||||
use Flarum\Http\Middleware\HandleErrors;
|
use Flarum\Http\Middleware\HandleErrors;
|
||||||
|
|
||||||
@ -30,8 +31,10 @@ class Server extends AbstractServer
|
|||||||
$errorDir = __DIR__ . '/../../error';
|
$errorDir = __DIR__ . '/../../error';
|
||||||
|
|
||||||
if ($app->isUpToDate()) {
|
if ($app->isUpToDate()) {
|
||||||
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie'));
|
|
||||||
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
|
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
|
||||||
|
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\AuthorizeWithCookie'));
|
||||||
|
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\StartSession'));
|
||||||
|
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\SetLocale'));
|
||||||
$pipe->pipe($adminPath, $app->make('Flarum\Admin\Middleware\RequireAdministrateAbility'));
|
$pipe->pipe($adminPath, $app->make('Flarum\Admin\Middleware\RequireAdministrateAbility'));
|
||||||
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.admin.routes')]));
|
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.admin.routes')]));
|
||||||
$pipe->pipe($adminPath, new HandleErrors($errorDir, $app->inDebugMode()));
|
$pipe->pipe($adminPath, new HandleErrors($errorDir, $app->inDebugMode()));
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\Api;
|
|
||||||
|
|
||||||
use Flarum\Database\AbstractModel;
|
|
||||||
use DateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property string $id
|
|
||||||
* @property int $user_id
|
|
||||||
* @property \Carbon\Carbon $created_at
|
|
||||||
* @property \Carbon\Carbon $expires_at
|
|
||||||
* @property \Flarum\Core\User|null $user
|
|
||||||
*/
|
|
||||||
class AccessToken extends AbstractModel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
protected $table = 'access_tokens';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use a custom primary key for this model.
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
public $incrementing = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
protected $dates = ['created_at', 'expires_at'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an access token for the specified user.
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @param int $minutes
|
|
||||||
* @return static
|
|
||||||
*/
|
|
||||||
public static function generate($userId, $minutes = 60)
|
|
||||||
{
|
|
||||||
$token = new static;
|
|
||||||
|
|
||||||
$token->id = str_random(40);
|
|
||||||
$token->user_id = $userId;
|
|
||||||
$token->created_at = time();
|
|
||||||
$token->expires_at = time() + $minutes * 60;
|
|
||||||
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that the token has not expired.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isValid()
|
|
||||||
{
|
|
||||||
return $this->expires_at > new DateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define the relationship with the owner of this access token.
|
|
||||||
*
|
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
||||||
*/
|
|
||||||
public function user()
|
|
||||||
{
|
|
||||||
return $this->belongsTo('Flarum\Core\User');
|
|
||||||
}
|
|
||||||
}
|
|
@ -44,9 +44,13 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
|
|
||||||
$handler->registerHandler(new Handler\FloodingExceptionHandler);
|
$handler->registerHandler(new Handler\FloodingExceptionHandler);
|
||||||
$handler->registerHandler(new Handler\IlluminateValidationExceptionHandler);
|
$handler->registerHandler(new Handler\IlluminateValidationExceptionHandler);
|
||||||
|
$handler->registerHandler(new Handler\InvalidAccessTokenExceptionHandler);
|
||||||
$handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler);
|
$handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler);
|
||||||
|
$handler->registerHandler(new Handler\MethodNotAllowedExceptionHandler);
|
||||||
$handler->registerHandler(new Handler\ModelNotFoundExceptionHandler);
|
$handler->registerHandler(new Handler\ModelNotFoundExceptionHandler);
|
||||||
$handler->registerHandler(new Handler\PermissionDeniedExceptionHandler);
|
$handler->registerHandler(new Handler\PermissionDeniedExceptionHandler);
|
||||||
|
$handler->registerHandler(new Handler\RouteNotFoundExceptionHandler);
|
||||||
|
$handler->registerHandler(new Handler\TokenMismatchExceptionHandler);
|
||||||
$handler->registerHandler(new Handler\ValidationExceptionHandler);
|
$handler->registerHandler(new Handler\ValidationExceptionHandler);
|
||||||
$handler->registerHandler(new InvalidParameterExceptionHandler);
|
$handler->registerHandler(new InvalidParameterExceptionHandler);
|
||||||
$handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode()));
|
$handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode()));
|
||||||
|
@ -12,6 +12,7 @@ namespace Flarum\Api;
|
|||||||
|
|
||||||
use Flarum\Http\Controller\ControllerInterface;
|
use Flarum\Http\Controller\ControllerInterface;
|
||||||
use Flarum\Core\User;
|
use Flarum\Core\User;
|
||||||
|
use Flarum\Http\Session;
|
||||||
use Illuminate\Contracts\Container\Container;
|
use Illuminate\Contracts\Container\Container;
|
||||||
use Exception;
|
use Exception;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
@ -43,14 +44,23 @@ class Client
|
|||||||
* Execute the given API action class, pass the input and return its response.
|
* Execute the given API action class, pass the input and return its response.
|
||||||
*
|
*
|
||||||
* @param string|ControllerInterface $controller
|
* @param string|ControllerInterface $controller
|
||||||
* @param User $actor
|
* @param Session|User|null $session
|
||||||
* @param array $queryParams
|
* @param array $queryParams
|
||||||
* @param array $body
|
* @param array $body
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
* @return \Psr\Http\Message\ResponseInterface
|
||||||
*/
|
*/
|
||||||
public function send($controller, User $actor, array $queryParams = [], array $body = [])
|
public function send($controller, $session, array $queryParams = [], array $body = [])
|
||||||
{
|
{
|
||||||
$request = ServerRequestFactory::fromGlobals(null, $queryParams, $body)->withAttribute('actor', $actor);
|
$request = ServerRequestFactory::fromGlobals(null, $queryParams, $body);
|
||||||
|
|
||||||
|
if ($session instanceof Session) {
|
||||||
|
$request = $request->withAttribute('session', $session);
|
||||||
|
$actor = $session->user;
|
||||||
|
} else {
|
||||||
|
$actor = $session;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = $request->withAttribute('actor', $actor);
|
||||||
|
|
||||||
if (is_string($controller)) {
|
if (is_string($controller)) {
|
||||||
$controller = $this->container->make($controller);
|
$controller = $this->container->make($controller);
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\Api\Command;
|
|
||||||
|
|
||||||
class GenerateAccessToken
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The ID of the user to generate an access token for.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $userId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $userId The ID of the user to generate an access token for.
|
|
||||||
*/
|
|
||||||
public function __construct($userId)
|
|
||||||
{
|
|
||||||
$this->userId = $userId;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\Api\Command;
|
|
||||||
|
|
||||||
use Flarum\Api\AccessToken;
|
|
||||||
use Flarum\Api\Command\GenerateAccessToken;
|
|
||||||
|
|
||||||
class GenerateAccessTokenHandler
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param GenerateAccessToken $command
|
|
||||||
* @return AccessToken
|
|
||||||
*/
|
|
||||||
public function handle(GenerateAccessToken $command)
|
|
||||||
{
|
|
||||||
$token = AccessToken::generate($command->userId);
|
|
||||||
|
|
||||||
$token->save();
|
|
||||||
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,12 +10,15 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
|
use Flarum\Core\Access\AssertPermissionTrait;
|
||||||
use Flarum\Core\Command\DeleteDiscussion;
|
use Flarum\Core\Command\DeleteDiscussion;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
class DeleteDiscussionController extends AbstractDeleteController
|
class DeleteDiscussionController extends AbstractDeleteController
|
||||||
{
|
{
|
||||||
|
use AssertPermissionTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Dispatcher
|
* @var Dispatcher
|
||||||
*/
|
*/
|
||||||
@ -38,6 +41,8 @@ class DeleteDiscussionController extends AbstractDeleteController
|
|||||||
$actor = $request->getAttribute('actor');
|
$actor = $request->getAttribute('actor');
|
||||||
$input = $request->getParsedBody();
|
$input = $request->getParsedBody();
|
||||||
|
|
||||||
|
$this->assertSudo($request);
|
||||||
|
|
||||||
$this->bus->dispatch(
|
$this->bus->dispatch(
|
||||||
new DeleteDiscussion($id, $actor, $input)
|
new DeleteDiscussion($id, $actor, $input)
|
||||||
);
|
);
|
||||||
|
@ -10,12 +10,15 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
|
use Flarum\Core\Access\AssertPermissionTrait;
|
||||||
use Flarum\Core\Command\DeleteGroup;
|
use Flarum\Core\Command\DeleteGroup;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
class DeleteGroupController extends AbstractDeleteController
|
class DeleteGroupController extends AbstractDeleteController
|
||||||
{
|
{
|
||||||
|
use AssertPermissionTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Dispatcher
|
* @var Dispatcher
|
||||||
*/
|
*/
|
||||||
@ -34,6 +37,8 @@ class DeleteGroupController extends AbstractDeleteController
|
|||||||
*/
|
*/
|
||||||
protected function delete(ServerRequestInterface $request)
|
protected function delete(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
|
$this->assertSudo($request);
|
||||||
|
|
||||||
$this->bus->dispatch(
|
$this->bus->dispatch(
|
||||||
new DeleteGroup(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
new DeleteGroup(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
||||||
);
|
);
|
||||||
|
@ -10,12 +10,15 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
|
use Flarum\Core\Access\AssertPermissionTrait;
|
||||||
use Flarum\Core\Command\DeletePost;
|
use Flarum\Core\Command\DeletePost;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
class DeletePostController extends AbstractDeleteController
|
class DeletePostController extends AbstractDeleteController
|
||||||
{
|
{
|
||||||
|
use AssertPermissionTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Dispatcher
|
* @var Dispatcher
|
||||||
*/
|
*/
|
||||||
@ -34,6 +37,8 @@ class DeletePostController extends AbstractDeleteController
|
|||||||
*/
|
*/
|
||||||
protected function delete(ServerRequestInterface $request)
|
protected function delete(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
|
$this->assertSudo($request);
|
||||||
|
|
||||||
$this->bus->dispatch(
|
$this->bus->dispatch(
|
||||||
new DeletePost(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
new DeletePost(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
||||||
);
|
);
|
||||||
|
@ -10,12 +10,15 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
|
use Flarum\Core\Access\AssertPermissionTrait;
|
||||||
use Flarum\Core\Command\DeleteUser;
|
use Flarum\Core\Command\DeleteUser;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
class DeleteUserController extends AbstractDeleteController
|
class DeleteUserController extends AbstractDeleteController
|
||||||
{
|
{
|
||||||
|
use AssertPermissionTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Dispatcher
|
* @var Dispatcher
|
||||||
*/
|
*/
|
||||||
@ -34,6 +37,8 @@ class DeleteUserController extends AbstractDeleteController
|
|||||||
*/
|
*/
|
||||||
protected function delete(ServerRequestInterface $request)
|
protected function delete(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
|
$this->assertSudo($request);
|
||||||
|
|
||||||
$this->bus->dispatch(
|
$this->bus->dispatch(
|
||||||
new DeleteUser(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
new DeleteUser(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
||||||
);
|
);
|
||||||
|
@ -25,7 +25,7 @@ class SetPermissionController implements ControllerInterface
|
|||||||
*/
|
*/
|
||||||
public function handle(ServerRequestInterface $request)
|
public function handle(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
$this->assertAdmin($request->getAttribute('actor'));
|
$this->assertAdminAndSudo($request);
|
||||||
|
|
||||||
$body = $request->getParsedBody();
|
$body = $request->getParsedBody();
|
||||||
$permission = array_get($body, 'permission');
|
$permission = array_get($body, 'permission');
|
||||||
|
@ -47,7 +47,7 @@ class SetSettingsController implements ControllerInterface
|
|||||||
*/
|
*/
|
||||||
public function handle(ServerRequestInterface $request)
|
public function handle(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
$this->assertAdmin($request->getAttribute('actor'));
|
$this->assertAdminAndSudo($request);
|
||||||
|
|
||||||
$settings = $request->getParsedBody();
|
$settings = $request->getParsedBody();
|
||||||
|
|
||||||
|
@ -10,11 +10,10 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
use Flarum\Api\Command\GenerateAccessToken;
|
|
||||||
use Flarum\Core\Exception\PermissionDeniedException;
|
use Flarum\Core\Exception\PermissionDeniedException;
|
||||||
use Flarum\Core\Repository\UserRepository;
|
use Flarum\Core\Repository\UserRepository;
|
||||||
use Flarum\Event\UserEmailChangeWasRequested;
|
|
||||||
use Flarum\Http\Controller\ControllerInterface;
|
use Flarum\Http\Controller\ControllerInterface;
|
||||||
|
use Flarum\Http\Session;
|
||||||
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 Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@ -65,19 +64,13 @@ class TokenController implements ControllerInterface
|
|||||||
throw new PermissionDeniedException;
|
throw new PermissionDeniedException;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $user->is_activated) {
|
$session = $request->getAttribute('session') ?: Session::generate($user);
|
||||||
$this->events->fire(new UserEmailChangeWasRequested($user, $user->email));
|
$session->assign($user)->regenerateId()->renew()->save();
|
||||||
|
|
||||||
return new JsonResponse(['emailConfirmationRequired' => $user->email], 401);
|
return (new JsonResponse([
|
||||||
}
|
'token' => $session->id,
|
||||||
|
|
||||||
$token = $this->bus->dispatch(
|
|
||||||
new GenerateAccessToken($user->id)
|
|
||||||
);
|
|
||||||
|
|
||||||
return new JsonResponse([
|
|
||||||
'token' => $token->id,
|
|
||||||
'userId' => $user->id
|
'userId' => $user->id
|
||||||
]);
|
]))
|
||||||
|
->withHeader('X-CSRF-Token', $session->csrf_token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ class UninstallExtensionController extends AbstractDeleteController
|
|||||||
|
|
||||||
protected function delete(ServerRequestInterface $request)
|
protected function delete(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
$this->assertAdmin($request->getAttribute('actor'));
|
$this->assertAdminAndSudo($request);
|
||||||
|
|
||||||
$name = array_get($request->getQueryParams(), 'name');
|
$name = array_get($request->getQueryParams(), 'name');
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class UpdateExtensionController implements ControllerInterface
|
|||||||
*/
|
*/
|
||||||
public function handle(ServerRequestInterface $request)
|
public function handle(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
$this->assertAdmin($request->getAttribute('actor'));
|
$this->assertAdminAndSudo($request);
|
||||||
|
|
||||||
$enabled = array_get($request->getParsedBody(), 'enabled');
|
$enabled = array_get($request->getParsedBody(), 'enabled');
|
||||||
$name = array_get($request->getQueryParams(), 'name');
|
$name = array_get($request->getQueryParams(), 'name');
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
|
use Flarum\Core\Access\AssertPermissionTrait;
|
||||||
use Flarum\Core\Command\EditUser;
|
use Flarum\Core\Command\EditUser;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@ -17,6 +18,8 @@ use Tobscure\JsonApi\Document;
|
|||||||
|
|
||||||
class UpdateUserController extends AbstractResourceController
|
class UpdateUserController extends AbstractResourceController
|
||||||
{
|
{
|
||||||
|
use AssertPermissionTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
@ -49,6 +52,8 @@ class UpdateUserController extends AbstractResourceController
|
|||||||
$actor = $request->getAttribute('actor');
|
$actor = $request->getAttribute('actor');
|
||||||
$data = array_get($request->getParsedBody(), 'data', []);
|
$data = array_get($request->getParsedBody(), 'data', []);
|
||||||
|
|
||||||
|
$this->assertSudo($request);
|
||||||
|
|
||||||
return $this->bus->dispatch(
|
return $this->bus->dispatch(
|
||||||
new EditUser($id, $actor, $data)
|
new EditUser($id, $actor, $data)
|
||||||
);
|
);
|
||||||
|
17
src/Api/Exception/InvalidAccessTokenException.php
Normal file
17
src/Api/Exception/InvalidAccessTokenException.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Api\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class InvalidAccessTokenException extends Exception
|
||||||
|
{
|
||||||
|
}
|
@ -31,7 +31,10 @@ class FloodingExceptionHandler implements ExceptionHandlerInterface
|
|||||||
public function handle(Exception $e)
|
public function handle(Exception $e)
|
||||||
{
|
{
|
||||||
$status = 429;
|
$status = 429;
|
||||||
$error = [];
|
$error = [
|
||||||
|
'status' => (string) $status,
|
||||||
|
'code' => 'too_many_requests'
|
||||||
|
];
|
||||||
|
|
||||||
return new ResponseBag($status, [$error]);
|
return new ResponseBag($status, [$error]);
|
||||||
}
|
}
|
||||||
|
@ -44,8 +44,10 @@ class IlluminateValidationExceptionHandler implements ExceptionHandlerInterface
|
|||||||
{
|
{
|
||||||
$errors = array_map(function ($field, $messages) {
|
$errors = array_map(function ($field, $messages) {
|
||||||
return [
|
return [
|
||||||
|
'status' => '422',
|
||||||
|
'code' => 'validation_error',
|
||||||
'detail' => implode("\n", $messages),
|
'detail' => implode("\n", $messages),
|
||||||
'source' => ['pointer' => '/data/attributes/' . $field],
|
'source' => ['pointer' => "/data/attributes/$field"]
|
||||||
];
|
];
|
||||||
}, array_keys($errors), $errors);
|
}, array_keys($errors), $errors);
|
||||||
|
|
||||||
|
41
src/Api/Handler/InvalidAccessTokenExceptionHandler.php
Normal file
41
src/Api/Handler/InvalidAccessTokenExceptionHandler.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Api\Handler;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Flarum\Api\Exception\InvalidAccessTokenException;
|
||||||
|
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
|
||||||
|
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
|
||||||
|
|
||||||
|
class InvalidAccessTokenExceptionHandler implements ExceptionHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function manages(Exception $e)
|
||||||
|
{
|
||||||
|
return $e instanceof InvalidAccessTokenException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function handle(Exception $e)
|
||||||
|
{
|
||||||
|
$status = 401;
|
||||||
|
$error = [
|
||||||
|
'status' => (string) $status,
|
||||||
|
'code' => 'invalid_access_token'
|
||||||
|
];
|
||||||
|
|
||||||
|
return new ResponseBag($status, [$error]);
|
||||||
|
}
|
||||||
|
}
|
@ -31,7 +31,10 @@ class InvalidConfirmationTokenExceptionHandler implements ExceptionHandlerInterf
|
|||||||
public function handle(Exception $e)
|
public function handle(Exception $e)
|
||||||
{
|
{
|
||||||
$status = 403;
|
$status = 403;
|
||||||
$error = ['code' => 'invalid_confirmation_token'];
|
$error = [
|
||||||
|
'status' => (string) $status,
|
||||||
|
'code' => 'invalid_confirmation_token'
|
||||||
|
];
|
||||||
|
|
||||||
return new ResponseBag($status, [$error]);
|
return new ResponseBag($status, [$error]);
|
||||||
}
|
}
|
||||||
|
41
src/Api/Handler/MethodNotAllowedExceptionHandler.php
Normal file
41
src/Api/Handler/MethodNotAllowedExceptionHandler.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Api\Handler;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Flarum\Http\Exception\MethodNotAllowedException;
|
||||||
|
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
|
||||||
|
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
|
||||||
|
|
||||||
|
class MethodNotAllowedExceptionHandler implements ExceptionHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function manages(Exception $e)
|
||||||
|
{
|
||||||
|
return $e instanceof MethodNotAllowedException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function handle(Exception $e)
|
||||||
|
{
|
||||||
|
$status = 405;
|
||||||
|
$error = [
|
||||||
|
'status' => (string) $status,
|
||||||
|
'code' => 'method_not_allowed'
|
||||||
|
];
|
||||||
|
|
||||||
|
return new ResponseBag($status, [$error]);
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@
|
|||||||
namespace Flarum\Api\Handler;
|
namespace Flarum\Api\Handler;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Flarum\Http\Exception\RouteNotFoundException;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
|
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
|
||||||
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
|
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
|
||||||
@ -31,7 +32,10 @@ class ModelNotFoundExceptionHandler implements ExceptionHandlerInterface
|
|||||||
public function handle(Exception $e)
|
public function handle(Exception $e)
|
||||||
{
|
{
|
||||||
$status = 404;
|
$status = 404;
|
||||||
$error = [];
|
$error = [
|
||||||
|
'status' => '404',
|
||||||
|
'code' => 'resource_not_found'
|
||||||
|
];
|
||||||
|
|
||||||
return new ResponseBag($status, [$error]);
|
return new ResponseBag($status, [$error]);
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,10 @@ class PermissionDeniedExceptionHandler implements ExceptionHandlerInterface
|
|||||||
public function handle(Exception $e)
|
public function handle(Exception $e)
|
||||||
{
|
{
|
||||||
$status = 401;
|
$status = 401;
|
||||||
$error = [];
|
$error = [
|
||||||
|
'status' => (string) $status,
|
||||||
|
'code' => 'permission_denied'
|
||||||
|
];
|
||||||
|
|
||||||
return new ResponseBag($status, [$error]);
|
return new ResponseBag($status, [$error]);
|
||||||
}
|
}
|
||||||
|
41
src/Api/Handler/RouteNotFoundExceptionHandler.php
Normal file
41
src/Api/Handler/RouteNotFoundExceptionHandler.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Api\Handler;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Flarum\Http\Exception\RouteNotFoundException;
|
||||||
|
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
|
||||||
|
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
|
||||||
|
|
||||||
|
class RouteNotFoundExceptionHandler implements ExceptionHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function manages(Exception $e)
|
||||||
|
{
|
||||||
|
return $e instanceof RouteNotFoundException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function handle(Exception $e)
|
||||||
|
{
|
||||||
|
$status = 404;
|
||||||
|
$error = [
|
||||||
|
'status' => (string) $status,
|
||||||
|
'code' => 'route_not_found'
|
||||||
|
];
|
||||||
|
|
||||||
|
return new ResponseBag($status, [$error]);
|
||||||
|
}
|
||||||
|
}
|
41
src/Api/Handler/TokenMismatchExceptionHandler.php
Normal file
41
src/Api/Handler/TokenMismatchExceptionHandler.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Api\Handler;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Flarum\Http\Exception\TokenMismatchException;
|
||||||
|
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
|
||||||
|
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
|
||||||
|
|
||||||
|
class TokenMismatchExceptionHandler implements ExceptionHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function manages(Exception $e)
|
||||||
|
{
|
||||||
|
return $e instanceof TokenMismatchException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function handle(Exception $e)
|
||||||
|
{
|
||||||
|
$status = 400;
|
||||||
|
$error = [
|
||||||
|
'status' => (string) $status,
|
||||||
|
'code' => 'csrf_token_mismatch'
|
||||||
|
];
|
||||||
|
|
||||||
|
return new ResponseBag($status, [$error]);
|
||||||
|
}
|
||||||
|
}
|
@ -33,10 +33,13 @@ class ValidationExceptionHandler implements ExceptionHandlerInterface
|
|||||||
$status = 422;
|
$status = 422;
|
||||||
|
|
||||||
$messages = $e->getMessages();
|
$messages = $e->getMessages();
|
||||||
$errors = array_map(function ($path, $detail) {
|
$errors = array_map(function ($path, $detail) use ($status) {
|
||||||
$source = ['pointer' => '/data/attributes/' . $path];
|
return [
|
||||||
|
'status' => (string) $status,
|
||||||
return compact('source', 'detail');
|
'code' => 'validation_error',
|
||||||
|
'detail' => $detail,
|
||||||
|
'source' => ['pointer' => "/data/attributes/$path"]
|
||||||
|
];
|
||||||
}, array_keys($messages), $messages);
|
}, array_keys($messages), $messages);
|
||||||
|
|
||||||
return new ResponseBag($status, $errors);
|
return new ResponseBag($status, $errors);
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\Api\Middleware;
|
|
||||||
|
|
||||||
use Flarum\Api\AccessToken;
|
|
||||||
use Flarum\Api\ApiKey;
|
|
||||||
use Flarum\Core\Guest;
|
|
||||||
use Flarum\Core\User;
|
|
||||||
use Flarum\Locale\LocaleManager;
|
|
||||||
use Illuminate\Contracts\Container\Container;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
use Zend\Stratigility\MiddlewareInterface;
|
|
||||||
|
|
||||||
class AuthenticateWithHeader implements MiddlewareInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $prefix = 'Token ';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var LocaleManager
|
|
||||||
*/
|
|
||||||
protected $locales;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param LocaleManager $locales
|
|
||||||
*/
|
|
||||||
public function __construct(LocaleManager $locales)
|
|
||||||
{
|
|
||||||
$this->locales = $locales;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
|
||||||
{
|
|
||||||
$request = $this->logIn($request);
|
|
||||||
|
|
||||||
return $out ? $out($request, $response) : $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @return Request
|
|
||||||
*/
|
|
||||||
protected function logIn(Request $request)
|
|
||||||
{
|
|
||||||
$header = $request->getHeaderLine('authorization');
|
|
||||||
|
|
||||||
$parts = explode(';', $header);
|
|
||||||
|
|
||||||
$actor = new Guest;
|
|
||||||
|
|
||||||
if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) {
|
|
||||||
$token = substr($parts[0], strlen($this->prefix));
|
|
||||||
|
|
||||||
if (($accessToken = AccessToken::find($token)) && $accessToken->isValid()) {
|
|
||||||
$actor = $accessToken->user;
|
|
||||||
|
|
||||||
$actor->updateLastSeen()->save();
|
|
||||||
} elseif (isset($parts[1]) && ($apiKey = ApiKey::valid($token))) {
|
|
||||||
$userParts = explode('=', trim($parts[1]));
|
|
||||||
|
|
||||||
if (isset($userParts[0]) && $userParts[0] === 'userId') {
|
|
||||||
$actor = User::find($userParts[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($actor->exists) {
|
|
||||||
$locale = $actor->getPreference('locale');
|
|
||||||
} else {
|
|
||||||
$locale = array_get($request->getCookieParams(), 'locale');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($locale && $this->locales->hasLocale($locale)) {
|
|
||||||
$this->locales->setLocale($locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $request->withAttribute('actor', $actor ?: new Guest);
|
|
||||||
}
|
|
||||||
}
|
|
@ -28,10 +28,12 @@ class Server extends AbstractServer
|
|||||||
$apiPath = parse_url($app->url('api'), PHP_URL_PATH);
|
$apiPath = parse_url($app->url('api'), PHP_URL_PATH);
|
||||||
|
|
||||||
if ($app->isInstalled() && $app->isUpToDate()) {
|
if ($app->isInstalled() && $app->isUpToDate()) {
|
||||||
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie'));
|
|
||||||
$pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\AuthenticateWithHeader'));
|
|
||||||
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
|
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
|
||||||
$pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\FakeHttpMethods'));
|
$pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\FakeHttpMethods'));
|
||||||
|
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\AuthorizeWithCookie'));
|
||||||
|
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\AuthorizeWithHeader'));
|
||||||
|
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\StartSession'));
|
||||||
|
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\SetLocale'));
|
||||||
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.api.routes')]));
|
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.api.routes')]));
|
||||||
$pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\HandleErrors'));
|
$pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\HandleErrors'));
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,8 +10,10 @@
|
|||||||
|
|
||||||
namespace Flarum\Core\Access;
|
namespace Flarum\Core\Access;
|
||||||
|
|
||||||
|
use Flarum\Api\Exception\InvalidAccessTokenException;
|
||||||
use Flarum\Core\Exception\PermissionDeniedException;
|
use Flarum\Core\Exception\PermissionDeniedException;
|
||||||
use Flarum\Core\User;
|
use Flarum\Core\User;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
trait AssertPermissionTrait
|
trait AssertPermissionTrait
|
||||||
{
|
{
|
||||||
@ -61,6 +63,30 @@ trait AssertPermissionTrait
|
|||||||
*/
|
*/
|
||||||
protected function assertAdmin(User $actor)
|
protected function assertAdmin(User $actor)
|
||||||
{
|
{
|
||||||
$this->assertPermission($actor->isAdmin());
|
$this->assertCan($actor, 'administrate');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @throws InvalidAccessTokenException
|
||||||
|
*/
|
||||||
|
protected function assertSudo(ServerRequestInterface $request)
|
||||||
|
{
|
||||||
|
$session = $request->getAttribute('session');
|
||||||
|
|
||||||
|
if (! $session || ! $session->isSudo()) {
|
||||||
|
throw new InvalidAccessTokenException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @throws PermissionDeniedException
|
||||||
|
*/
|
||||||
|
protected function assertAdminAndSudo(ServerRequestInterface $request)
|
||||||
|
{
|
||||||
|
$this->assertAdmin($request->getAttribute('actor'));
|
||||||
|
|
||||||
|
$this->assertSudo($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ class User extends AbstractModel
|
|||||||
|
|
||||||
$user->read()->detach();
|
$user->read()->detach();
|
||||||
$user->groups()->detach();
|
$user->groups()->detach();
|
||||||
$user->accessTokens()->delete();
|
$user->sessions()->delete();
|
||||||
$user->notifications()->delete();
|
$user->notifications()->delete();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -654,13 +654,13 @@ class User extends AbstractModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the relationship with the user's access tokens.
|
* Define the relationship with the user's sessions.
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
*/
|
*/
|
||||||
public function accessTokens()
|
public function sessions()
|
||||||
{
|
{
|
||||||
return $this->hasMany('Flarum\Api\AccessToken');
|
return $this->hasMany('Flarum\Http\Session');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,16 +11,17 @@
|
|||||||
namespace Flarum\Event;
|
namespace Flarum\Event;
|
||||||
|
|
||||||
use Flarum\Core\User;
|
use Flarum\Core\User;
|
||||||
|
use Flarum\Http\Session;
|
||||||
|
|
||||||
class UserLoggedIn
|
class UserLoggedIn
|
||||||
{
|
{
|
||||||
public $user;
|
public $user;
|
||||||
|
|
||||||
public $token;
|
public $session;
|
||||||
|
|
||||||
public function __construct(User $user, $token)
|
public function __construct(User $user, Session $session)
|
||||||
{
|
{
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
$this->token = $token;
|
$this->session = $session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,14 +12,11 @@ namespace Flarum\Forum\Controller;
|
|||||||
|
|
||||||
use Flarum\Core\User;
|
use Flarum\Core\User;
|
||||||
use Zend\Diactoros\Response\HtmlResponse;
|
use Zend\Diactoros\Response\HtmlResponse;
|
||||||
use Flarum\Api\Command\GenerateAccessToken;
|
|
||||||
use Flarum\Core\AuthToken;
|
use Flarum\Core\AuthToken;
|
||||||
use DateTime;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
|
||||||
trait AuthenticateUserTrait
|
trait AuthenticateUserTrait
|
||||||
{
|
{
|
||||||
use WriteRememberCookieTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Illuminate\Contracts\Bus\Dispatcher
|
* @var \Illuminate\Contracts\Bus\Dispatcher
|
||||||
*/
|
*/
|
||||||
@ -45,7 +42,7 @@ trait AuthenticateUserTrait
|
|||||||
* @param array $suggestions
|
* @param array $suggestions
|
||||||
* @return HtmlResponse
|
* @return HtmlResponse
|
||||||
*/
|
*/
|
||||||
protected function authenticate(array $identification, array $suggestions = [])
|
protected function authenticate(Request $request, array $identification, array $suggestions = [])
|
||||||
{
|
{
|
||||||
$user = User::where($identification)->first();
|
$user = User::where($identification)->first();
|
||||||
|
|
||||||
@ -70,13 +67,8 @@ trait AuthenticateUserTrait
|
|||||||
$response = new HtmlResponse($content);
|
$response = new HtmlResponse($content);
|
||||||
|
|
||||||
if ($user) {
|
if ($user) {
|
||||||
// Extend the token's expiry to 2 weeks so that we can set a
|
$session = $request->getAttribute('session');
|
||||||
// remember cookie
|
$session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save();
|
||||||
$accessToken = $this->bus->dispatch(new GenerateAccessToken($user->id));
|
|
||||||
$accessToken::unguard();
|
|
||||||
$accessToken->update(['expires_at' => new DateTime('+2 weeks')]);
|
|
||||||
|
|
||||||
$response = $this->withRememberCookie($response, $accessToken->id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
namespace Flarum\Forum\Controller;
|
namespace Flarum\Forum\Controller;
|
||||||
|
|
||||||
use Flarum\Core\Command\ConfirmEmail;
|
use Flarum\Core\Command\ConfirmEmail;
|
||||||
use Flarum\Api\Command\GenerateAccessToken;
|
|
||||||
use Flarum\Core\Exception\InvalidConfirmationTokenException;
|
use Flarum\Core\Exception\InvalidConfirmationTokenException;
|
||||||
use Flarum\Foundation\Application;
|
use Flarum\Foundation\Application;
|
||||||
use Flarum\Http\Controller\ControllerInterface;
|
use Flarum\Http\Controller\ControllerInterface;
|
||||||
@ -22,8 +21,6 @@ use Zend\Diactoros\Response\RedirectResponse;
|
|||||||
|
|
||||||
class ConfirmEmailController implements ControllerInterface
|
class ConfirmEmailController implements ControllerInterface
|
||||||
{
|
{
|
||||||
use WriteRememberCookieTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Dispatcher
|
* @var Dispatcher
|
||||||
*/
|
*/
|
||||||
@ -60,13 +57,9 @@ class ConfirmEmailController implements ControllerInterface
|
|||||||
return new HtmlResponse('Invalid confirmation token');
|
return new HtmlResponse('Invalid confirmation token');
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = $this->bus->dispatch(
|
$session = $request->getAttribute('session');
|
||||||
new GenerateAccessToken($user->id)
|
$session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save();
|
||||||
);
|
|
||||||
|
|
||||||
return $this->withRememberCookie(
|
return new RedirectResponse($this->app->url());
|
||||||
new RedirectResponse($this->app->url()),
|
|
||||||
$token->id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,19 +11,16 @@
|
|||||||
namespace Flarum\Forum\Controller;
|
namespace Flarum\Forum\Controller;
|
||||||
|
|
||||||
use Flarum\Api\Client;
|
use Flarum\Api\Client;
|
||||||
use Flarum\Api\AccessToken;
|
use Flarum\Http\Session;
|
||||||
use Flarum\Event\UserLoggedIn;
|
use Flarum\Event\UserLoggedIn;
|
||||||
use Flarum\Core\Repository\UserRepository;
|
use Flarum\Core\Repository\UserRepository;
|
||||||
use Flarum\Http\Controller\ControllerInterface;
|
use Flarum\Http\Controller\ControllerInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Zend\Diactoros\Response\EmptyResponse;
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
use DateTime;
|
|
||||||
|
|
||||||
class LoginController implements ControllerInterface
|
class LogInController implements ControllerInterface
|
||||||
{
|
{
|
||||||
use WriteRememberCookieTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Flarum\Core\Repository\UserRepository
|
* @var \Flarum\Core\Repository\UserRepository
|
||||||
*/
|
*/
|
||||||
@ -52,26 +49,20 @@ class LoginController implements ControllerInterface
|
|||||||
public function handle(Request $request, array $routeParams = [])
|
public function handle(Request $request, array $routeParams = [])
|
||||||
{
|
{
|
||||||
$controller = 'Flarum\Api\Controller\TokenController';
|
$controller = 'Flarum\Api\Controller\TokenController';
|
||||||
$actor = $request->getAttribute('actor');
|
$session = $request->getAttribute('session');
|
||||||
$params = array_only($request->getParsedBody(), ['identification', 'password']);
|
$params = array_only($request->getParsedBody(), ['identification', 'password']);
|
||||||
|
|
||||||
$response = $this->apiClient->send($controller, $actor, [], $params);
|
$response = $this->apiClient->send($controller, $session, [], $params);
|
||||||
|
|
||||||
if ($response->getStatusCode() === 200) {
|
if ($response->getStatusCode() === 200) {
|
||||||
$data = json_decode($response->getBody());
|
$data = json_decode($response->getBody());
|
||||||
|
|
||||||
// Extend the token's expiry to 2 weeks so that we can set a
|
$session = Session::find($data->token);
|
||||||
// remember cookie
|
$session->setDuration(60 * 24 * 14)->save();
|
||||||
AccessToken::where('id', $data->token)->update(['expires_at' => new DateTime('+2 weeks')]);
|
|
||||||
|
|
||||||
event(new UserLoggedIn($this->users->findOrFail($data->userId), $data->token));
|
event(new UserLoggedIn($this->users->findOrFail($data->userId), $session));
|
||||||
|
|
||||||
return $this->withRememberCookie(
|
|
||||||
$response,
|
|
||||||
$data->token
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return $response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,18 +10,16 @@
|
|||||||
|
|
||||||
namespace Flarum\Forum\Controller;
|
namespace Flarum\Forum\Controller;
|
||||||
|
|
||||||
use Flarum\Api\AccessToken;
|
|
||||||
use Flarum\Event\UserLoggedOut;
|
use Flarum\Event\UserLoggedOut;
|
||||||
use Flarum\Foundation\Application;
|
use Flarum\Foundation\Application;
|
||||||
use Flarum\Http\Controller\ControllerInterface;
|
use Flarum\Http\Controller\ControllerInterface;
|
||||||
|
use Flarum\Http\Exception\TokenMismatchException;
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Zend\Diactoros\Response\RedirectResponse;
|
use Zend\Diactoros\Response\RedirectResponse;
|
||||||
|
|
||||||
class LogoutController implements ControllerInterface
|
class LogOutController implements ControllerInterface
|
||||||
{
|
{
|
||||||
use WriteRememberCookieTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Application
|
* @var Application
|
||||||
*/
|
*/
|
||||||
@ -46,21 +44,24 @@ class LogoutController implements ControllerInterface
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param array $routeParams
|
* @param array $routeParams
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
* @return \Psr\Http\Message\ResponseInterface
|
||||||
|
* @throws TokenMismatchException
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, array $routeParams = [])
|
public function handle(Request $request, array $routeParams = [])
|
||||||
{
|
{
|
||||||
$user = $request->getAttribute('actor');
|
$session = $request->getAttribute('session');
|
||||||
|
|
||||||
if ($user->exists) {
|
if ($user = $session->user) {
|
||||||
$token = array_get($request->getQueryParams(), 'token');
|
if (array_get($request->getQueryParams(), 'token') !== $session->csrf_token) {
|
||||||
|
throw new TokenMismatchException;
|
||||||
|
}
|
||||||
|
|
||||||
AccessToken::where('user_id', $user->id)->findOrFail($token);
|
$session->exists = false;
|
||||||
|
|
||||||
$user->accessTokens()->delete();
|
$user->sessions()->delete();
|
||||||
|
|
||||||
$this->events->fire(new UserLoggedOut($user));
|
$this->events->fire(new UserLoggedOut($user));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->withForgetCookie(new RedirectResponse($this->app->url()));
|
return new RedirectResponse($this->app->url());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,19 +11,15 @@
|
|||||||
namespace Flarum\Forum\Controller;
|
namespace Flarum\Forum\Controller;
|
||||||
|
|
||||||
use Flarum\Api\Client;
|
use Flarum\Api\Client;
|
||||||
use Flarum\Api\AccessToken;
|
use Flarum\Core\User;
|
||||||
use Flarum\Http\Controller\ControllerInterface;
|
use Flarum\Http\Controller\ControllerInterface;
|
||||||
use Flarum\Api\Command\GenerateAccessToken;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Zend\Diactoros\Response\EmptyResponse;
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use DateTime;
|
|
||||||
|
|
||||||
class RegisterController implements ControllerInterface
|
class RegisterController implements ControllerInterface
|
||||||
{
|
{
|
||||||
use WriteRememberCookieTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Dispatcher
|
* @var Dispatcher
|
||||||
*/
|
*/
|
||||||
@ -61,21 +57,13 @@ class RegisterController implements ControllerInterface
|
|||||||
$body = json_decode($response->getBody());
|
$body = json_decode($response->getBody());
|
||||||
$statusCode = $response->getStatusCode();
|
$statusCode = $response->getStatusCode();
|
||||||
|
|
||||||
$response = new JsonResponse($body, $statusCode);
|
if (isset($body->data)) {
|
||||||
|
$user = User::find($body->data->id);
|
||||||
|
|
||||||
if (! empty($body->data->attributes->isActivated)) {
|
$session = $request->getAttribute('session');
|
||||||
$token = $this->bus->dispatch(new GenerateAccessToken($body->data->id));
|
$session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save();
|
||||||
|
|
||||||
// Extend the token's expiry to 2 weeks so that we can set a
|
|
||||||
// remember cookie
|
|
||||||
AccessToken::where('id', $token->id)->update(['expires_at' => new DateTime('+2 weeks')]);
|
|
||||||
|
|
||||||
return $this->withRememberCookie(
|
|
||||||
$response,
|
|
||||||
$token->id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response;
|
return new JsonResponse($body, $statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\Forum\Controller;
|
|
||||||
|
|
||||||
use Dflydev\FigCookies\FigResponseCookies;
|
|
||||||
use Dflydev\FigCookies\SetCookie;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
|
|
||||||
trait WriteRememberCookieTrait
|
|
||||||
{
|
|
||||||
protected function withRememberCookie(ResponseInterface $response, $token)
|
|
||||||
{
|
|
||||||
// Set a long-living cookie (two weeks) with the remember token
|
|
||||||
return FigResponseCookies::set(
|
|
||||||
$response,
|
|
||||||
SetCookie::create('flarum_remember', $token)
|
|
||||||
->withMaxAge(14 * 24 * 60 * 60)
|
|
||||||
->withPath('/')
|
|
||||||
->withHttpOnly(true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function withForgetCookie(ResponseInterface $response)
|
|
||||||
{
|
|
||||||
// Delete the cookie by setting it to an expiration date in the past
|
|
||||||
return FigResponseCookies::set(
|
|
||||||
$response,
|
|
||||||
SetCookie::create('flarum_remember')
|
|
||||||
->withMaxAge(-2628000)
|
|
||||||
->withPath('/')
|
|
||||||
->withHttpOnly(true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -35,8 +35,10 @@ class Server extends AbstractServer
|
|||||||
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.install.routes')]));
|
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.install.routes')]));
|
||||||
$pipe->pipe($basePath, new HandleErrors($errorDir, true));
|
$pipe->pipe($basePath, new HandleErrors($errorDir, true));
|
||||||
} elseif ($app->isUpToDate()) {
|
} elseif ($app->isUpToDate()) {
|
||||||
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie'));
|
|
||||||
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
|
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
|
||||||
|
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\AuthorizeWithCookie'));
|
||||||
|
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\StartSession'));
|
||||||
|
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\SetLocale'));
|
||||||
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.forum.routes')]));
|
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.forum.routes')]));
|
||||||
$pipe->pipe($basePath, new HandleErrors($errorDir, $app->inDebugMode()));
|
$pipe->pipe($basePath, new HandleErrors($errorDir, $app->inDebugMode()));
|
||||||
} else {
|
} else {
|
||||||
|
@ -339,9 +339,11 @@ class ClientView implements Renderable
|
|||||||
*/
|
*/
|
||||||
protected function getSession()
|
protected function getSession()
|
||||||
{
|
{
|
||||||
|
$session = $this->request->getAttribute('session');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'userId' => $this->actor->id,
|
'userId' => $this->actor->id,
|
||||||
'token' => array_get($this->request->getCookieParams(), 'flarum_remember'),
|
'csrfToken' => $session->csrf_token
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
src/Http/Exception/MethodNotAllowedException.php
Normal file
22
src/Http/Exception/MethodNotAllowedException.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Http\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class MethodNotAllowedException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($message = null, $code = 405, Exception $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
18
src/Http/Exception/TokenMismatchException.php
Normal file
18
src/Http/Exception/TokenMismatchException.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Http\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class TokenMismatchException extends Exception
|
||||||
|
{
|
||||||
|
}
|
@ -10,82 +10,43 @@
|
|||||||
|
|
||||||
namespace Flarum\Http\Middleware;
|
namespace Flarum\Http\Middleware;
|
||||||
|
|
||||||
use Flarum\Api\AccessToken;
|
use Flarum\Http\Exception\TokenMismatchException;
|
||||||
use Flarum\Core\Guest;
|
use Flarum\Http\Session;
|
||||||
use Flarum\Locale\LocaleManager;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Zend\Stratigility\MiddlewareInterface;
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
class AuthenticateWithCookie implements MiddlewareInterface
|
class AuthenticateWithCookie implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var LocaleManager
|
|
||||||
*/
|
|
||||||
protected $locales;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param LocaleManager $locales
|
|
||||||
*/
|
|
||||||
public function __construct(LocaleManager $locales)
|
|
||||||
{
|
|
||||||
$this->locales = $locales;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
{
|
{
|
||||||
$request = $this->logIn($request);
|
$id = array_get($request->getCookieParams(), 'flarum_session');
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$session = Session::find($id);
|
||||||
|
|
||||||
|
$request = $request->withAttribute('session', $session);
|
||||||
|
|
||||||
|
if (! $this->isReading($request) && ! $this->tokensMatch($request)) {
|
||||||
|
throw new TokenMismatchException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $out ? $out($request, $response) : $response;
|
return $out ? $out($request, $response) : $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function isReading(Request $request)
|
||||||
* Set the application's actor instance according to the request token.
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return Request
|
|
||||||
*/
|
|
||||||
protected function logIn(Request $request)
|
|
||||||
{
|
{
|
||||||
$actor = new Guest;
|
return in_array($request->getMethod(), ['HEAD', 'GET', 'OPTIONS']);
|
||||||
|
|
||||||
if ($token = $this->getToken($request)) {
|
|
||||||
if (! $token->isValid()) {
|
|
||||||
// TODO: https://github.com/flarum/core/issues/253
|
|
||||||
} elseif ($token->user) {
|
|
||||||
$actor = $token->user;
|
|
||||||
$actor->updateLastSeen()->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($actor->exists) {
|
|
||||||
$locale = $actor->getPreference('locale');
|
|
||||||
} else {
|
|
||||||
$locale = array_get($request->getCookieParams(), 'locale');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($locale && $this->locales->hasLocale($locale)) {
|
|
||||||
$this->locales->setLocale($locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $request->withAttribute('actor', $actor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function tokensMatch(Request $request)
|
||||||
* Get the access token referred to by the request cookie.
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return AccessToken|null
|
|
||||||
*/
|
|
||||||
protected function getToken(Request $request)
|
|
||||||
{
|
{
|
||||||
$token = array_get($request->getCookieParams(), 'flarum_remember');
|
$input = $request->getHeaderLine('X-CSRF-Token') ?: array_get($request->getParsedBody(), 'token');
|
||||||
|
|
||||||
if ($token) {
|
return $request->getAttribute('session')->csrf_token === $input;
|
||||||
return AccessToken::find($token);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61
src/Http/Middleware/AuthenticateWithHeader.php
Normal file
61
src/Http/Middleware/AuthenticateWithHeader.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Http\Middleware;
|
||||||
|
|
||||||
|
use Flarum\Api\ApiKey;
|
||||||
|
use Flarum\Core\User;
|
||||||
|
use Flarum\Http\Session;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
|
class AuthenticateWithHeader implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $prefix = 'Token ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
|
{
|
||||||
|
$headerLine = $request->getHeaderLine('authorization');
|
||||||
|
|
||||||
|
$parts = explode(';', $headerLine);
|
||||||
|
|
||||||
|
if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) {
|
||||||
|
$id = substr($parts[0], strlen($this->prefix));
|
||||||
|
|
||||||
|
if (isset($parts[1]) && ApiKey::valid($id)) {
|
||||||
|
if ($actor = $this->getUser($parts[1])) {
|
||||||
|
$request = $request->withAttribute('actor', $actor);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$session = Session::find($id);
|
||||||
|
|
||||||
|
$request = $request->withAttribute('session', $session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out ? $out($request, $response) : $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUser($string)
|
||||||
|
{
|
||||||
|
$parts = explode('=', trim($string));
|
||||||
|
|
||||||
|
if (isset($parts[0]) && $parts[0] === 'userId') {
|
||||||
|
return User::find($parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,8 +13,9 @@ namespace Flarum\Http\Middleware;
|
|||||||
|
|
||||||
use FastRoute\Dispatcher;
|
use FastRoute\Dispatcher;
|
||||||
use FastRoute\RouteParser;
|
use FastRoute\RouteParser;
|
||||||
use Flarum\Http\RouteCollection;
|
use Flarum\Http\Exception\MethodNotAllowedException;
|
||||||
use Flarum\Http\Exception\RouteNotFoundException;
|
use Flarum\Http\Exception\RouteNotFoundException;
|
||||||
|
use Flarum\Http\RouteCollection;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ class DispatchRoute
|
|||||||
* @param Response $response
|
* @param Response $response
|
||||||
* @param callable $out
|
* @param callable $out
|
||||||
* @return Response
|
* @return Response
|
||||||
|
* @throws MethodNotAllowedException
|
||||||
* @throws RouteNotFoundException
|
* @throws RouteNotFoundException
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
@ -58,8 +60,11 @@ class DispatchRoute
|
|||||||
|
|
||||||
switch ($routeInfo[0]) {
|
switch ($routeInfo[0]) {
|
||||||
case Dispatcher::NOT_FOUND:
|
case Dispatcher::NOT_FOUND:
|
||||||
case Dispatcher::METHOD_NOT_ALLOWED:
|
|
||||||
throw new RouteNotFoundException;
|
throw new RouteNotFoundException;
|
||||||
|
|
||||||
|
case Dispatcher::METHOD_NOT_ALLOWED:
|
||||||
|
throw new MethodNotAllowedException;
|
||||||
|
|
||||||
case Dispatcher::FOUND:
|
case Dispatcher::FOUND:
|
||||||
$handler = $routeInfo[1];
|
$handler = $routeInfo[1];
|
||||||
$parameters = $routeInfo[2];
|
$parameters = $routeInfo[2];
|
||||||
|
52
src/Http/Middleware/SetLocale.php
Normal file
52
src/Http/Middleware/SetLocale.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Http\Middleware;
|
||||||
|
|
||||||
|
use Flarum\Locale\LocaleManager;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
|
class SetLocale implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var LocaleManager
|
||||||
|
*/
|
||||||
|
protected $locales;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param LocaleManager $locales
|
||||||
|
*/
|
||||||
|
public function __construct(LocaleManager $locales)
|
||||||
|
{
|
||||||
|
$this->locales = $locales;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
|
{
|
||||||
|
$actor = $request->getAttribute('actor');
|
||||||
|
|
||||||
|
if ($actor->exists) {
|
||||||
|
$locale = $actor->getPreference('locale');
|
||||||
|
} else {
|
||||||
|
$locale = array_get($request->getCookieParams(), 'locale');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($locale && $this->locales->hasLocale($locale)) {
|
||||||
|
$this->locales->setLocale($locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out ? $out($request, $response) : $response;
|
||||||
|
}
|
||||||
|
}
|
81
src/Http/Middleware/StartSession.php
Normal file
81
src/Http/Middleware/StartSession.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Http\Middleware;
|
||||||
|
|
||||||
|
use Dflydev\FigCookies\FigResponseCookies;
|
||||||
|
use Dflydev\FigCookies\SetCookie;
|
||||||
|
use Dflydev\FigCookies\SetCookies;
|
||||||
|
use Flarum\Http\Session;
|
||||||
|
use Flarum\Core\Guest;
|
||||||
|
use Flarum\Http\WriteSessionCookieTrait;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
|
class StartSession implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
use WriteSessionCookieTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
|
{
|
||||||
|
$this->collectGarbage();
|
||||||
|
|
||||||
|
$session = $this->getSession($request);
|
||||||
|
$actor = $this->getActor($session);
|
||||||
|
|
||||||
|
$request = $request
|
||||||
|
->withAttribute('session', $session)
|
||||||
|
->withAttribute('actor', $actor);
|
||||||
|
|
||||||
|
$response = $out ? $out($request, $response) : $response;
|
||||||
|
|
||||||
|
return $this->addSessionCookieToResponse($response, $session, 'flarum_session');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSession(Request $request)
|
||||||
|
{
|
||||||
|
$session = $request->getAttribute('session');
|
||||||
|
|
||||||
|
if (! $session) {
|
||||||
|
$session = Session::generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->extend()->save();
|
||||||
|
|
||||||
|
return $session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getActor(Session $session)
|
||||||
|
{
|
||||||
|
$actor = $session->user ?: new Guest;
|
||||||
|
|
||||||
|
if ($actor->exists) {
|
||||||
|
$actor->updateLastSeen()->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function collectGarbage()
|
||||||
|
{
|
||||||
|
if ($this->hitsLottery()) {
|
||||||
|
Session::whereRaw('last_activity <= ? - duration * 60', [time()])->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hitsLottery()
|
||||||
|
{
|
||||||
|
return mt_rand(1, 100) <= 1;
|
||||||
|
}
|
||||||
|
}
|
140
src/Http/Session.php
Normal file
140
src/Http/Session.php
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Http;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Flarum\Core\User;
|
||||||
|
use Flarum\Database\AbstractModel;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property int $last_activity
|
||||||
|
* @property int $duration
|
||||||
|
* @property \Carbon\Carbon $sudo_expiry_time
|
||||||
|
* @property string $csrf_token
|
||||||
|
* @property \Flarum\Core\User|null $user
|
||||||
|
*/
|
||||||
|
class Session extends AbstractModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected $table = 'sessions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a custom primary key for this model.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected $dates = ['sudo_expiry_time'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a session.
|
||||||
|
*
|
||||||
|
* @param User|null $user
|
||||||
|
* @param int $duration How long before the session will expire, in minutes.
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function generate(User $user = null, $duration = 60)
|
||||||
|
{
|
||||||
|
$session = new static;
|
||||||
|
|
||||||
|
$session->assign($user)
|
||||||
|
->regenerateId()
|
||||||
|
->renew()
|
||||||
|
->setDuration($duration);
|
||||||
|
|
||||||
|
return $session->extend();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign the session to a user.
|
||||||
|
*
|
||||||
|
* @param User|null $user
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function assign(User $user = null)
|
||||||
|
{
|
||||||
|
$this->user_id = $user ? $user->id : null;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate the session ID.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function regenerateId()
|
||||||
|
{
|
||||||
|
$this->id = sha1(uniqid('', true).Str::random(25).microtime(true));
|
||||||
|
$this->csrf_token = Str::random(40);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function extend()
|
||||||
|
{
|
||||||
|
$this->last_activity = time();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function renew()
|
||||||
|
{
|
||||||
|
$this->extend();
|
||||||
|
$this->sudo_expiry_time = time() + 30 * 60;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $duration How long before the session will expire, in minutes.
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setDuration($duration)
|
||||||
|
{
|
||||||
|
$this->duration = $duration;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isSudo()
|
||||||
|
{
|
||||||
|
return $this->sudo_expiry_time > new DateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the relationship with the owner of this access token.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
29
src/Http/WriteSessionCookieTrait.php
Normal file
29
src/Http/WriteSessionCookieTrait.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\Http;
|
||||||
|
|
||||||
|
use Dflydev\FigCookies\FigResponseCookies;
|
||||||
|
use Dflydev\FigCookies\SetCookie;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
|
||||||
|
trait WriteSessionCookieTrait
|
||||||
|
{
|
||||||
|
protected function addSessionCookieToResponse(Response $response, Session $session, $cookieName)
|
||||||
|
{
|
||||||
|
return FigResponseCookies::set(
|
||||||
|
$response,
|
||||||
|
SetCookie::create($cookieName, $session->exists ? $session->id : null)
|
||||||
|
->withMaxAge($session->exists ? $session->duration * 60 : -2628000)
|
||||||
|
->withPath('/')
|
||||||
|
->withHttpOnly(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,14 +10,15 @@
|
|||||||
|
|
||||||
namespace Flarum\Install\Controller;
|
namespace Flarum\Install\Controller;
|
||||||
|
|
||||||
|
use Flarum\Core\User;
|
||||||
use Flarum\Http\Controller\ControllerInterface;
|
use Flarum\Http\Controller\ControllerInterface;
|
||||||
|
use Flarum\Http\Session;
|
||||||
|
use Flarum\Http\WriteSessionCookieTrait;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Zend\Diactoros\Response\HtmlResponse;
|
use Zend\Diactoros\Response\HtmlResponse;
|
||||||
use Zend\Diactoros\Response;
|
use Zend\Diactoros\Response;
|
||||||
use Flarum\Install\Console\InstallCommand;
|
use Flarum\Install\Console\InstallCommand;
|
||||||
use Flarum\Install\Console\DefaultsDataProvider;
|
use Flarum\Install\Console\DefaultsDataProvider;
|
||||||
use Flarum\Api\Command\GenerateAccessToken;
|
|
||||||
use Flarum\Forum\Controller\WriteRememberCookieTrait;
|
|
||||||
use Symfony\Component\Console\Output\StreamOutput;
|
use Symfony\Component\Console\Output\StreamOutput;
|
||||||
use Symfony\Component\Console\Input\StringInput;
|
use Symfony\Component\Console\Input\StringInput;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
@ -26,7 +27,7 @@ use DateTime;
|
|||||||
|
|
||||||
class InstallController implements ControllerInterface
|
class InstallController implements ControllerInterface
|
||||||
{
|
{
|
||||||
use WriteRememberCookieTrait;
|
use WriteSessionCookieTrait;
|
||||||
|
|
||||||
protected $command;
|
protected $command;
|
||||||
|
|
||||||
@ -87,14 +88,9 @@ class InstallController implements ControllerInterface
|
|||||||
return new HtmlResponse($e->getMessage(), 500);
|
return new HtmlResponse($e->getMessage(), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = $this->bus->dispatch(
|
$session = Session::generate(User::find(1), 60 * 24 * 14);
|
||||||
new GenerateAccessToken(1)
|
$session->save();
|
||||||
);
|
|
||||||
$token->update(['expires_at' => new DateTime('+2 weeks')]);
|
|
||||||
|
|
||||||
return $this->withRememberCookie(
|
return $this->addSessionCookieToResponse(new Response($body, 200), $session, 'flarum_session');
|
||||||
new Response($body, 200),
|
|
||||||
$token->id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
views/login.blade.php
Normal file
32
views/login.blade.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Log In</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Log In</h1>
|
||||||
|
|
||||||
|
<form class="form-horizontal" role="form" method="POST" action="{{ app('Flarum\Admin\UrlGenerator')->toRoute('index') }}">
|
||||||
|
<input type="hidden" name="token" value="{{ $token }}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">Username or Email</label>
|
||||||
|
<input type="text" class="form-control" name="identification">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">Password</label>
|
||||||
|
<input type="password" class="form-control" name="password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Log In</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user