Merge branch 'sudo-mode'

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

View File

@ -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});
} }
} }

View File

@ -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) {

View File

@ -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}>

View File

@ -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();
}
});
} }
}, },

View File

@ -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();
});
} }
}; };

View File

@ -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;
} }
/** /**

View File

@ -150,7 +150,7 @@ export default class Model {
// Before we update the model's data, we should make a copy of the model's // 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.
* *

View File

@ -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);
}
} }
} }

View File

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

View File

@ -98,7 +98,10 @@ export default class Modal extends Component {
* Focus on the first input when the modal is ready to be used. * 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() {
} }
/** /**

View File

@ -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();

View File

@ -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
); );
} }

View File

@ -167,6 +167,9 @@
color: @muted-more-color; color: @muted-more-color;
} }
} }
.Post--loading {
opacity: 0.5;
}
.PostMeta { .PostMeta {
display: inline; display: inline;
} }

View 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');
});
}
}

View 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');
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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()));

View File

@ -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');
}
}

View File

@ -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()));

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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)
); );

View File

@ -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'))
); );

View File

@ -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'))
); );

View File

@ -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'))
); );

View File

@ -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');

View File

@ -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();

View File

@ -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);
} }
} }

View File

@ -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');

View File

@ -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');

View File

@ -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)
); );

View 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
{
}

View File

@ -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]);
} }

View File

@ -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);

View 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]);
}
}

View File

@ -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]);
} }

View 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]);
}
}

View File

@ -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]);
} }

View File

@ -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]);
} }

View 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]);
}
}

View 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]);
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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);
} }
} }

View File

@ -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');
} }
/** /**

View File

@ -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;
} }
} }

View File

@ -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;

View File

@ -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
);
} }
} }

View File

@ -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;
} }
} }

View File

@ -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());
} }
} }

View File

@ -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);
} }
} }

View File

@ -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)
);
}
}

View File

@ -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 {

View File

@ -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
]; ];
} }
} }

View 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);
}
}

View 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
{
}

View File

@ -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);
}
} }
} }

View 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]);
}
}
}

View File

@ -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];

View 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;
}
}

View 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
View 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);
}
}

View 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)
);
}
}

View File

@ -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
View 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>