mirror of
https://github.com/flarum/framework.git
synced 2025-05-22 06:39:57 +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;
|
||||
}
|
||||
|
||||
const oldEmail = app.session.user.email();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.session.user.save({email: this.email()}, {errorHandler: this.onerror.bind(this)})
|
||||
.then(() => this.success = true)
|
||||
.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 password = this.password();
|
||||
|
||||
app.session.login(email, password, {errorHandler: this.onerror.bind(this)})
|
||||
.catch(this.loaded.bind(this));
|
||||
app.session.login(email, password, {errorHandler: this.onerror.bind(this)}).then(
|
||||
() => window.location.reload(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
onerror(error) {
|
||||
|
@ -18,6 +18,8 @@ import ItemList from 'flarum/utils/ItemList';
|
||||
*/
|
||||
export default class Post extends Component {
|
||||
init() {
|
||||
this.loading = false;
|
||||
|
||||
/**
|
||||
* Set up a subtree retainer so that the post will not be redrawn
|
||||
* unless new data comes in.
|
||||
@ -37,7 +39,7 @@ export default class Post extends Component {
|
||||
view() {
|
||||
const attrs = this.attrs();
|
||||
|
||||
attrs.className = 'Post ' + (attrs.className || '');
|
||||
attrs.className = 'Post ' + (this.loading ? 'Post--loading ' : '') + (attrs.className || '');
|
||||
|
||||
return (
|
||||
<article {...attrs}>
|
||||
|
@ -217,18 +217,19 @@ export default {
|
||||
*/
|
||||
deleteAction() {
|
||||
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
|
||||
// to the previous page.
|
||||
if (app.viewingDiscussion(this)) {
|
||||
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}
|
||||
* @protected
|
||||
*/
|
||||
destructiveControls(post) {
|
||||
destructiveControls(post, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (post.contentType() === 'comment' && !post.isHidden()) {
|
||||
@ -101,7 +101,7 @@ export default {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'times',
|
||||
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}
|
||||
*/
|
||||
deleteAction() {
|
||||
this.discussion().removePost(this.id());
|
||||
deleteAction(context) {
|
||||
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 Button from 'flarum/components/Button';
|
||||
import RequestErrorModal from 'flarum/components/RequestErrorModal';
|
||||
import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal';
|
||||
import Translator from 'flarum/Translator';
|
||||
import extract from 'flarum/utils/extract';
|
||||
import patchMithril from 'flarum/utils/patchMithril';
|
||||
@ -182,14 +183,17 @@ export default class App {
|
||||
* @return {Promise}
|
||||
* @public
|
||||
*/
|
||||
request(options) {
|
||||
request(originalOptions) {
|
||||
const options = Object.assign({}, originalOptions);
|
||||
|
||||
// Set some default options if they haven't been overridden. We want to
|
||||
// authenticate all requests with the session token. We also want all
|
||||
// requests to run asynchronously in the background, so that they don't
|
||||
// prevent redraws from occurring.
|
||||
options.config = options.config || this.session.authorize.bind(this.session);
|
||||
options.background = options.background || true;
|
||||
|
||||
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken));
|
||||
|
||||
// If the method is something like PATCH or DELETE, which not all servers
|
||||
// and clients support, then we'll send it as a POST request with the
|
||||
// intended method specified in the X-HTTP-Method-Override header.
|
||||
@ -218,7 +222,7 @@ export default class App {
|
||||
if (original) {
|
||||
responseText = original(xhr.responseText);
|
||||
} else {
|
||||
responseText = xhr.responseText.length > 0 ? xhr.responseText : null;
|
||||
responseText = xhr.responseText || null;
|
||||
}
|
||||
|
||||
const status = xhr.status;
|
||||
@ -227,6 +231,11 @@ export default class App {
|
||||
throw new RequestError(status, responseText, options, xhr);
|
||||
}
|
||||
|
||||
if (xhr.getResponseHeader) {
|
||||
const csrfToken = xhr.getResponseHeader('X-CSRF-Token');
|
||||
if (csrfToken) app.session.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(responseText);
|
||||
} catch (e) {
|
||||
@ -238,9 +247,20 @@ export default class App {
|
||||
|
||||
// Now make the request. If it's a failure, inspect the error that was
|
||||
// returned and show an alert containing its contents.
|
||||
return m.request(options).then(null, error => {
|
||||
const deferred = m.deferred();
|
||||
|
||||
m.request(options).then(response => deferred.resolve(response), error => {
|
||||
this.requestError = error;
|
||||
|
||||
if (error.response && error.response.errors && error.response.errors[0] && error.response.errors[0].code === 'invalid_access_token') {
|
||||
this.modal.show(new ConfirmPasswordModal({
|
||||
deferredRequest: originalOptions,
|
||||
deferred,
|
||||
error
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
let children;
|
||||
|
||||
switch (error.status) {
|
||||
@ -283,8 +303,10 @@ export default class App {
|
||||
this.alerts.show(error.alert);
|
||||
}
|
||||
|
||||
throw error;
|
||||
deferred.reject(error);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -150,7 +150,7 @@ export default class Model {
|
||||
// Before we update the model's data, we should make a copy of the model's
|
||||
// old data so that we can revert back to it if something goes awry during
|
||||
// persistence.
|
||||
const oldData = JSON.parse(JSON.stringify(this.data));
|
||||
const oldData = this.copyData();
|
||||
|
||||
this.pushData(data);
|
||||
|
||||
@ -209,6 +209,10 @@ export default class Model {
|
||||
return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
|
||||
}
|
||||
|
||||
copyData() {
|
||||
return JSON.parse(JSON.stringify(this.data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a function which returns the value of the given attribute.
|
||||
*
|
||||
|
@ -3,7 +3,7 @@
|
||||
* to the current authenticated user, and provides methods to log in/out.
|
||||
*/
|
||||
export default class Session {
|
||||
constructor(token, user) {
|
||||
constructor(user, csrfToken) {
|
||||
/**
|
||||
* The current authenticated user.
|
||||
*
|
||||
@ -13,12 +13,12 @@ export default class Session {
|
||||
this.user = user;
|
||||
|
||||
/**
|
||||
* The token that was used for authentication.
|
||||
* The CSRF token.
|
||||
*
|
||||
* @type {String|null}
|
||||
* @public
|
||||
*/
|
||||
this.token = token;
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,8 +35,7 @@ export default class Session {
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('baseUrl') + '/login',
|
||||
data: {identification, password}
|
||||
}, options))
|
||||
.then(() => window.location.reload());
|
||||
}, options));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,19 +44,6 @@ export default class Session {
|
||||
* @public
|
||||
*/
|
||||
logout() {
|
||||
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an authorization header with the current token to the given
|
||||
* XMLHttpRequest object.
|
||||
*
|
||||
* @param {XMLHttpRequest} xhr
|
||||
* @public
|
||||
*/
|
||||
authorize(xhr) {
|
||||
if (this.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
|
||||
}
|
||||
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
|
||||
}
|
||||
}
|
||||
|
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.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
clear() {
|
||||
if (this.component) {
|
||||
this.component.onhide();
|
||||
}
|
||||
|
||||
this.component = null;
|
||||
|
||||
m.lazyRedraw();
|
||||
|
@ -18,7 +18,7 @@ export default function preload(app) {
|
||||
app.forum = app.store.getById('forums', 1);
|
||||
|
||||
app.session = new Session(
|
||||
app.preload.session.token,
|
||||
app.store.getById('users', app.preload.session.userId)
|
||||
app.store.getById('users', app.preload.session.userId),
|
||||
app.preload.session.csrfToken
|
||||
);
|
||||
}
|
||||
|
@ -167,6 +167,9 @@
|
||||
color: @muted-more-color;
|
||||
}
|
||||
}
|
||||
.Post--loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.PostMeta {
|
||||
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()
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin');
|
||||
|
||||
$this->flushAssetsWhenThemeChanged();
|
||||
|
||||
$this->flushAssetsWhenExtensionsChanged();
|
||||
|
@ -10,26 +10,39 @@
|
||||
|
||||
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\View\Factory;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
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;
|
||||
|
||||
class RequireAdministrateAbility implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var Gate
|
||||
*/
|
||||
protected $gate;
|
||||
use AssertPermissionTrait;
|
||||
|
||||
/**
|
||||
* @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)
|
||||
{
|
||||
$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')) {
|
||||
throw new PermissionDeniedException;
|
||||
if ($response->getStatusCode() === 200) {
|
||||
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;
|
||||
|
@ -13,6 +13,7 @@ namespace Flarum\Admin;
|
||||
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Http\AbstractServer;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
use Zend\Stratigility\MiddlewarePipe;
|
||||
use Flarum\Http\Middleware\HandleErrors;
|
||||
|
||||
@ -30,8 +31,10 @@ class Server extends AbstractServer
|
||||
$errorDir = __DIR__ . '/../../error';
|
||||
|
||||
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\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\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.admin.routes')]));
|
||||
$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\IlluminateValidationExceptionHandler);
|
||||
$handler->registerHandler(new Handler\InvalidAccessTokenExceptionHandler);
|
||||
$handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler);
|
||||
$handler->registerHandler(new Handler\MethodNotAllowedExceptionHandler);
|
||||
$handler->registerHandler(new Handler\ModelNotFoundExceptionHandler);
|
||||
$handler->registerHandler(new Handler\PermissionDeniedExceptionHandler);
|
||||
$handler->registerHandler(new Handler\RouteNotFoundExceptionHandler);
|
||||
$handler->registerHandler(new Handler\TokenMismatchExceptionHandler);
|
||||
$handler->registerHandler(new Handler\ValidationExceptionHandler);
|
||||
$handler->registerHandler(new InvalidParameterExceptionHandler);
|
||||
$handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode()));
|
||||
|
@ -12,6 +12,7 @@ namespace Flarum\Api;
|
||||
|
||||
use Flarum\Http\Controller\ControllerInterface;
|
||||
use Flarum\Core\User;
|
||||
use Flarum\Http\Session;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
@ -43,14 +44,23 @@ class Client
|
||||
* Execute the given API action class, pass the input and return its response.
|
||||
*
|
||||
* @param string|ControllerInterface $controller
|
||||
* @param User $actor
|
||||
* @param Session|User|null $session
|
||||
* @param array $queryParams
|
||||
* @param array $body
|
||||
* @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)) {
|
||||
$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;
|
||||
|
||||
use Flarum\Core\Access\AssertPermissionTrait;
|
||||
use Flarum\Core\Command\DeleteDiscussion;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteDiscussionController extends AbstractDeleteController
|
||||
{
|
||||
use AssertPermissionTrait;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
@ -38,6 +41,8 @@ class DeleteDiscussionController extends AbstractDeleteController
|
||||
$actor = $request->getAttribute('actor');
|
||||
$input = $request->getParsedBody();
|
||||
|
||||
$this->assertSudo($request);
|
||||
|
||||
$this->bus->dispatch(
|
||||
new DeleteDiscussion($id, $actor, $input)
|
||||
);
|
||||
|
@ -10,12 +10,15 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Core\Access\AssertPermissionTrait;
|
||||
use Flarum\Core\Command\DeleteGroup;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteGroupController extends AbstractDeleteController
|
||||
{
|
||||
use AssertPermissionTrait;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
@ -34,6 +37,8 @@ class DeleteGroupController extends AbstractDeleteController
|
||||
*/
|
||||
protected function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$this->assertSudo($request);
|
||||
|
||||
$this->bus->dispatch(
|
||||
new DeleteGroup(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
||||
);
|
||||
|
@ -10,12 +10,15 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Core\Access\AssertPermissionTrait;
|
||||
use Flarum\Core\Command\DeletePost;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeletePostController extends AbstractDeleteController
|
||||
{
|
||||
use AssertPermissionTrait;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
@ -34,6 +37,8 @@ class DeletePostController extends AbstractDeleteController
|
||||
*/
|
||||
protected function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$this->assertSudo($request);
|
||||
|
||||
$this->bus->dispatch(
|
||||
new DeletePost(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
||||
);
|
||||
|
@ -10,12 +10,15 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Core\Access\AssertPermissionTrait;
|
||||
use Flarum\Core\Command\DeleteUser;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteUserController extends AbstractDeleteController
|
||||
{
|
||||
use AssertPermissionTrait;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
@ -34,6 +37,8 @@ class DeleteUserController extends AbstractDeleteController
|
||||
*/
|
||||
protected function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$this->assertSudo($request);
|
||||
|
||||
$this->bus->dispatch(
|
||||
new DeleteUser(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
|
||||
);
|
||||
|
@ -25,7 +25,7 @@ class SetPermissionController implements ControllerInterface
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request)
|
||||
{
|
||||
$this->assertAdmin($request->getAttribute('actor'));
|
||||
$this->assertAdminAndSudo($request);
|
||||
|
||||
$body = $request->getParsedBody();
|
||||
$permission = array_get($body, 'permission');
|
||||
|
@ -47,7 +47,7 @@ class SetSettingsController implements ControllerInterface
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request)
|
||||
{
|
||||
$this->assertAdmin($request->getAttribute('actor'));
|
||||
$this->assertAdminAndSudo($request);
|
||||
|
||||
$settings = $request->getParsedBody();
|
||||
|
||||
|
@ -10,11 +10,10 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\Command\GenerateAccessToken;
|
||||
use Flarum\Core\Exception\PermissionDeniedException;
|
||||
use Flarum\Core\Repository\UserRepository;
|
||||
use Flarum\Event\UserEmailChangeWasRequested;
|
||||
use Flarum\Http\Controller\ControllerInterface;
|
||||
use Flarum\Http\Session;
|
||||
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
|
||||
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
@ -65,19 +64,13 @@ class TokenController implements ControllerInterface
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
|
||||
if (! $user->is_activated) {
|
||||
$this->events->fire(new UserEmailChangeWasRequested($user, $user->email));
|
||||
$session = $request->getAttribute('session') ?: Session::generate($user);
|
||||
$session->assign($user)->regenerateId()->renew()->save();
|
||||
|
||||
return new JsonResponse(['emailConfirmationRequired' => $user->email], 401);
|
||||
}
|
||||
|
||||
$token = $this->bus->dispatch(
|
||||
new GenerateAccessToken($user->id)
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'token' => $token->id,
|
||||
return (new JsonResponse([
|
||||
'token' => $session->id,
|
||||
'userId' => $user->id
|
||||
]);
|
||||
]))
|
||||
->withHeader('X-CSRF-Token', $session->csrf_token);
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class UninstallExtensionController extends AbstractDeleteController
|
||||
|
||||
protected function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$this->assertAdmin($request->getAttribute('actor'));
|
||||
$this->assertAdminAndSudo($request);
|
||||
|
||||
$name = array_get($request->getQueryParams(), 'name');
|
||||
|
||||
|
@ -38,7 +38,7 @@ class UpdateExtensionController implements ControllerInterface
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request)
|
||||
{
|
||||
$this->assertAdmin($request->getAttribute('actor'));
|
||||
$this->assertAdminAndSudo($request);
|
||||
|
||||
$enabled = array_get($request->getParsedBody(), 'enabled');
|
||||
$name = array_get($request->getQueryParams(), 'name');
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Core\Access\AssertPermissionTrait;
|
||||
use Flarum\Core\Command\EditUser;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
@ -17,6 +18,8 @@ use Tobscure\JsonApi\Document;
|
||||
|
||||
class UpdateUserController extends AbstractResourceController
|
||||
{
|
||||
use AssertPermissionTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@ -49,6 +52,8 @@ class UpdateUserController extends AbstractResourceController
|
||||
$actor = $request->getAttribute('actor');
|
||||
$data = array_get($request->getParsedBody(), 'data', []);
|
||||
|
||||
$this->assertSudo($request);
|
||||
|
||||
return $this->bus->dispatch(
|
||||
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)
|
||||
{
|
||||
$status = 429;
|
||||
$error = [];
|
||||
$error = [
|
||||
'status' => (string) $status,
|
||||
'code' => 'too_many_requests'
|
||||
];
|
||||
|
||||
return new ResponseBag($status, [$error]);
|
||||
}
|
||||
|
@ -44,8 +44,10 @@ class IlluminateValidationExceptionHandler implements ExceptionHandlerInterface
|
||||
{
|
||||
$errors = array_map(function ($field, $messages) {
|
||||
return [
|
||||
'status' => '422',
|
||||
'code' => 'validation_error',
|
||||
'detail' => implode("\n", $messages),
|
||||
'source' => ['pointer' => '/data/attributes/' . $field],
|
||||
'source' => ['pointer' => "/data/attributes/$field"]
|
||||
];
|
||||
}, 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)
|
||||
{
|
||||
$status = 403;
|
||||
$error = ['code' => 'invalid_confirmation_token'];
|
||||
$error = [
|
||||
'status' => (string) $status,
|
||||
'code' => 'invalid_confirmation_token'
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
use Exception;
|
||||
use Flarum\Http\Exception\RouteNotFoundException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
|
||||
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
|
||||
@ -31,7 +32,10 @@ class ModelNotFoundExceptionHandler implements ExceptionHandlerInterface
|
||||
public function handle(Exception $e)
|
||||
{
|
||||
$status = 404;
|
||||
$error = [];
|
||||
$error = [
|
||||
'status' => '404',
|
||||
'code' => 'resource_not_found'
|
||||
];
|
||||
|
||||
return new ResponseBag($status, [$error]);
|
||||
}
|
||||
|
@ -31,7 +31,10 @@ class PermissionDeniedExceptionHandler implements ExceptionHandlerInterface
|
||||
public function handle(Exception $e)
|
||||
{
|
||||
$status = 401;
|
||||
$error = [];
|
||||
$error = [
|
||||
'status' => (string) $status,
|
||||
'code' => 'permission_denied'
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
$messages = $e->getMessages();
|
||||
$errors = array_map(function ($path, $detail) {
|
||||
$source = ['pointer' => '/data/attributes/' . $path];
|
||||
|
||||
return compact('source', 'detail');
|
||||
$errors = array_map(function ($path, $detail) use ($status) {
|
||||
return [
|
||||
'status' => (string) $status,
|
||||
'code' => 'validation_error',
|
||||
'detail' => $detail,
|
||||
'source' => ['pointer' => "/data/attributes/$path"]
|
||||
];
|
||||
}, array_keys($messages), $messages);
|
||||
|
||||
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);
|
||||
|
||||
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\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\Api\Middleware\HandleErrors'));
|
||||
} else {
|
||||
|
@ -10,8 +10,10 @@
|
||||
|
||||
namespace Flarum\Core\Access;
|
||||
|
||||
use Flarum\Api\Exception\InvalidAccessTokenException;
|
||||
use Flarum\Core\Exception\PermissionDeniedException;
|
||||
use Flarum\Core\User;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
trait AssertPermissionTrait
|
||||
{
|
||||
@ -61,6 +63,30 @@ trait AssertPermissionTrait
|
||||
*/
|
||||
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->groups()->detach();
|
||||
$user->accessTokens()->delete();
|
||||
$user->sessions()->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
|
||||
*/
|
||||
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;
|
||||
|
||||
use Flarum\Core\User;
|
||||
use Flarum\Http\Session;
|
||||
|
||||
class UserLoggedIn
|
||||
{
|
||||
public $user;
|
||||
|
||||
public $token;
|
||||
public $session;
|
||||
|
||||
public function __construct(User $user, $token)
|
||||
public function __construct(User $user, Session $session)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->token = $token;
|
||||
$this->session = $session;
|
||||
}
|
||||
}
|
||||
|
@ -12,14 +12,11 @@ namespace Flarum\Forum\Controller;
|
||||
|
||||
use Flarum\Core\User;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
use Flarum\Api\Command\GenerateAccessToken;
|
||||
use Flarum\Core\AuthToken;
|
||||
use DateTime;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
trait AuthenticateUserTrait
|
||||
{
|
||||
use WriteRememberCookieTrait;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Bus\Dispatcher
|
||||
*/
|
||||
@ -45,7 +42,7 @@ trait AuthenticateUserTrait
|
||||
* @param array $suggestions
|
||||
* @return HtmlResponse
|
||||
*/
|
||||
protected function authenticate(array $identification, array $suggestions = [])
|
||||
protected function authenticate(Request $request, array $identification, array $suggestions = [])
|
||||
{
|
||||
$user = User::where($identification)->first();
|
||||
|
||||
@ -70,13 +67,8 @@ trait AuthenticateUserTrait
|
||||
$response = new HtmlResponse($content);
|
||||
|
||||
if ($user) {
|
||||
// Extend the token's expiry to 2 weeks so that we can set a
|
||||
// remember cookie
|
||||
$accessToken = $this->bus->dispatch(new GenerateAccessToken($user->id));
|
||||
$accessToken::unguard();
|
||||
$accessToken->update(['expires_at' => new DateTime('+2 weeks')]);
|
||||
|
||||
$response = $this->withRememberCookie($response, $accessToken->id);
|
||||
$session = $request->getAttribute('session');
|
||||
$session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save();
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
@ -11,7 +11,6 @@
|
||||
namespace Flarum\Forum\Controller;
|
||||
|
||||
use Flarum\Core\Command\ConfirmEmail;
|
||||
use Flarum\Api\Command\GenerateAccessToken;
|
||||
use Flarum\Core\Exception\InvalidConfirmationTokenException;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Http\Controller\ControllerInterface;
|
||||
@ -22,8 +21,6 @@ use Zend\Diactoros\Response\RedirectResponse;
|
||||
|
||||
class ConfirmEmailController implements ControllerInterface
|
||||
{
|
||||
use WriteRememberCookieTrait;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
@ -60,13 +57,9 @@ class ConfirmEmailController implements ControllerInterface
|
||||
return new HtmlResponse('Invalid confirmation token');
|
||||
}
|
||||
|
||||
$token = $this->bus->dispatch(
|
||||
new GenerateAccessToken($user->id)
|
||||
);
|
||||
$session = $request->getAttribute('session');
|
||||
$session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save();
|
||||
|
||||
return $this->withRememberCookie(
|
||||
new RedirectResponse($this->app->url()),
|
||||
$token->id
|
||||
);
|
||||
return new RedirectResponse($this->app->url());
|
||||
}
|
||||
}
|
||||
|
@ -11,19 +11,16 @@
|
||||
namespace Flarum\Forum\Controller;
|
||||
|
||||
use Flarum\Api\Client;
|
||||
use Flarum\Api\AccessToken;
|
||||
use Flarum\Http\Session;
|
||||
use Flarum\Event\UserLoggedIn;
|
||||
use Flarum\Core\Repository\UserRepository;
|
||||
use Flarum\Http\Controller\ControllerInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use DateTime;
|
||||
|
||||
class LoginController implements ControllerInterface
|
||||
class LogInController implements ControllerInterface
|
||||
{
|
||||
use WriteRememberCookieTrait;
|
||||
|
||||
/**
|
||||
* @var \Flarum\Core\Repository\UserRepository
|
||||
*/
|
||||
@ -52,26 +49,20 @@ class LoginController implements ControllerInterface
|
||||
public function handle(Request $request, array $routeParams = [])
|
||||
{
|
||||
$controller = 'Flarum\Api\Controller\TokenController';
|
||||
$actor = $request->getAttribute('actor');
|
||||
$session = $request->getAttribute('session');
|
||||
$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) {
|
||||
$data = json_decode($response->getBody());
|
||||
|
||||
// Extend the token's expiry to 2 weeks so that we can set a
|
||||
// remember cookie
|
||||
AccessToken::where('id', $data->token)->update(['expires_at' => new DateTime('+2 weeks')]);
|
||||
$session = Session::find($data->token);
|
||||
$session->setDuration(60 * 24 * 14)->save();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,18 +10,16 @@
|
||||
|
||||
namespace Flarum\Forum\Controller;
|
||||
|
||||
use Flarum\Api\AccessToken;
|
||||
use Flarum\Event\UserLoggedOut;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Http\Controller\ControllerInterface;
|
||||
use Flarum\Http\Exception\TokenMismatchException;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\RedirectResponse;
|
||||
|
||||
class LogoutController implements ControllerInterface
|
||||
class LogOutController implements ControllerInterface
|
||||
{
|
||||
use WriteRememberCookieTrait;
|
||||
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
@ -46,21 +44,24 @@ class LogoutController implements ControllerInterface
|
||||
* @param Request $request
|
||||
* @param array $routeParams
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
* @throws TokenMismatchException
|
||||
*/
|
||||
public function handle(Request $request, array $routeParams = [])
|
||||
{
|
||||
$user = $request->getAttribute('actor');
|
||||
$session = $request->getAttribute('session');
|
||||
|
||||
if ($user->exists) {
|
||||
$token = array_get($request->getQueryParams(), 'token');
|
||||
if ($user = $session->user) {
|
||||
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));
|
||||
}
|
||||
|
||||
return $this->withForgetCookie(new RedirectResponse($this->app->url()));
|
||||
return new RedirectResponse($this->app->url());
|
||||
}
|
||||
}
|
@ -11,19 +11,15 @@
|
||||
namespace Flarum\Forum\Controller;
|
||||
|
||||
use Flarum\Api\Client;
|
||||
use Flarum\Api\AccessToken;
|
||||
use Flarum\Core\User;
|
||||
use Flarum\Http\Controller\ControllerInterface;
|
||||
use Flarum\Api\Command\GenerateAccessToken;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use DateTime;
|
||||
|
||||
class RegisterController implements ControllerInterface
|
||||
{
|
||||
use WriteRememberCookieTrait;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
@ -61,21 +57,13 @@ class RegisterController implements ControllerInterface
|
||||
$body = json_decode($response->getBody());
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
$response = new JsonResponse($body, $statusCode);
|
||||
if (isset($body->data)) {
|
||||
$user = User::find($body->data->id);
|
||||
|
||||
if (! empty($body->data->attributes->isActivated)) {
|
||||
$token = $this->bus->dispatch(new GenerateAccessToken($body->data->id));
|
||||
|
||||
// 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
|
||||
);
|
||||
$session = $request->getAttribute('session');
|
||||
$session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save();
|
||||
}
|
||||
|
||||
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, new HandleErrors($errorDir, true));
|
||||
} 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\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, new HandleErrors($errorDir, $app->inDebugMode()));
|
||||
} else {
|
||||
|
@ -339,9 +339,11 @@ class ClientView implements Renderable
|
||||
*/
|
||||
protected function getSession()
|
||||
{
|
||||
$session = $this->request->getAttribute('session');
|
||||
|
||||
return [
|
||||
'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;
|
||||
|
||||
use Flarum\Api\AccessToken;
|
||||
use Flarum\Core\Guest;
|
||||
use Flarum\Locale\LocaleManager;
|
||||
use Flarum\Http\Exception\TokenMismatchException;
|
||||
use Flarum\Http\Session;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class AuthenticateWithCookie 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)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application's actor instance according to the request token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Request
|
||||
*/
|
||||
protected function logIn(Request $request)
|
||||
private function isReading(Request $request)
|
||||
{
|
||||
$actor = new Guest;
|
||||
|
||||
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();
|
||||
}
|
||||
return in_array($request->getMethod(), ['HEAD', 'GET', 'OPTIONS']);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the access token referred to by the request cookie.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return AccessToken|null
|
||||
*/
|
||||
protected function getToken(Request $request)
|
||||
private function tokensMatch(Request $request)
|
||||
{
|
||||
$token = array_get($request->getCookieParams(), 'flarum_remember');
|
||||
$input = $request->getHeaderLine('X-CSRF-Token') ?: array_get($request->getParsedBody(), 'token');
|
||||
|
||||
if ($token) {
|
||||
return AccessToken::find($token);
|
||||
}
|
||||
return $request->getAttribute('session')->csrf_token === $input;
|
||||
}
|
||||
}
|
||||
|
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\RouteParser;
|
||||
use Flarum\Http\RouteCollection;
|
||||
use Flarum\Http\Exception\MethodNotAllowedException;
|
||||
use Flarum\Http\Exception\RouteNotFoundException;
|
||||
use Flarum\Http\RouteCollection;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
@ -47,6 +48,7 @@ class DispatchRoute
|
||||
* @param Response $response
|
||||
* @param callable $out
|
||||
* @return Response
|
||||
* @throws MethodNotAllowedException
|
||||
* @throws RouteNotFoundException
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
@ -58,8 +60,11 @@ class DispatchRoute
|
||||
|
||||
switch ($routeInfo[0]) {
|
||||
case Dispatcher::NOT_FOUND:
|
||||
case Dispatcher::METHOD_NOT_ALLOWED:
|
||||
throw new RouteNotFoundException;
|
||||
|
||||
case Dispatcher::METHOD_NOT_ALLOWED:
|
||||
throw new MethodNotAllowedException;
|
||||
|
||||
case Dispatcher::FOUND:
|
||||
$handler = $routeInfo[1];
|
||||
$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;
|
||||
|
||||
use Flarum\Core\User;
|
||||
use Flarum\Http\Controller\ControllerInterface;
|
||||
use Flarum\Http\Session;
|
||||
use Flarum\Http\WriteSessionCookieTrait;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
use Zend\Diactoros\Response;
|
||||
use Flarum\Install\Console\InstallCommand;
|
||||
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\Input\StringInput;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
@ -26,7 +27,7 @@ use DateTime;
|
||||
|
||||
class InstallController implements ControllerInterface
|
||||
{
|
||||
use WriteRememberCookieTrait;
|
||||
use WriteSessionCookieTrait;
|
||||
|
||||
protected $command;
|
||||
|
||||
@ -87,14 +88,9 @@ class InstallController implements ControllerInterface
|
||||
return new HtmlResponse($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$token = $this->bus->dispatch(
|
||||
new GenerateAccessToken(1)
|
||||
);
|
||||
$token->update(['expires_at' => new DateTime('+2 weeks')]);
|
||||
$session = Session::generate(User::find(1), 60 * 24 * 14);
|
||||
$session->save();
|
||||
|
||||
return $this->withRememberCookie(
|
||||
new Response($body, 200),
|
||||
$token->id
|
||||
);
|
||||
return $this->addSessionCookieToResponse(new Response($body, 200), $session, 'flarum_session');
|
||||
}
|
||||
}
|
||||
|
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