diff --git a/framework/core/js/admin/src/components/AppearancePage.js b/framework/core/js/admin/src/components/AppearancePage.js
index 67b1884e0..2b6fd436c 100644
--- a/framework/core/js/admin/src/components/AppearancePage.js
+++ b/framework/core/js/admin/src/components/AppearancePage.js
@@ -44,6 +44,7 @@ export default class AppearancePage extends Component {
{Button.component({
className: 'Button Button--primary',
+ type: 'submit',
children: 'Save Changes',
loading: this.loading
})}
diff --git a/framework/core/js/admin/src/components/EditCustomCssModal.js b/framework/core/js/admin/src/components/EditCustomCssModal.js
index 1619a66ec..651a4f183 100644
--- a/framework/core/js/admin/src/components/EditCustomCssModal.js
+++ b/framework/core/js/admin/src/components/EditCustomCssModal.js
@@ -30,6 +30,7 @@ export default class EditCustomCssModal extends Modal {
{Button.component({
className: 'Button Button--primary',
+ type: 'submit',
children: 'Save Changes',
loading: this.loading
})}
diff --git a/framework/core/js/forum/src/ForumApp.js b/framework/core/js/forum/src/ForumApp.js
index 9e7edd397..2abeea524 100644
--- a/framework/core/js/forum/src/ForumApp.js
+++ b/framework/core/js/forum/src/ForumApp.js
@@ -4,6 +4,7 @@ import Search from 'flarum/components/Search';
import Composer from 'flarum/components/Composer';
import ReplyComposer from 'flarum/components/ReplyComposer';
import DiscussionPage from 'flarum/components/DiscussionPage';
+import SignUpModal from 'flarum/components/SignUpModal';
export default class ForumApp extends App {
constructor(...args) {
@@ -76,4 +77,27 @@ export default class ForumApp extends App {
return this.current instanceof DiscussionPage &&
this.current.discussion === discussion;
}
+
+ /**
+ * Callback for when an external authenticator (social login) action has
+ * completed.
+ *
+ * If the payload indicates that the user has been logged in, then the page
+ * will be reloaded. Otherwise, a SignUpModal will be opened, prefilled
+ * with the provided details.
+ *
+ * @param {Object} payload A dictionary of props to pass into the sign up
+ * modal. A truthy `authenticated` prop indicates that the user has logged
+ * in, and thus the page is reloaded.
+ * @public
+ */
+ authenticationComplete(payload) {
+ if (payload.authenticated) {
+ window.location.reload();
+ } else {
+ const modal = new SignUpModal(payload);
+ this.modal.show(modal);
+ modal.$('[name=password]').focus();
+ }
+ }
}
diff --git a/framework/core/js/forum/src/components/LogInButton.js b/framework/core/js/forum/src/components/LogInButton.js
new file mode 100644
index 000000000..3de450589
--- /dev/null
+++ b/framework/core/js/forum/src/components/LogInButton.js
@@ -0,0 +1,30 @@
+import Button from 'flarum/components/Button';
+
+/**
+ * The `LogInButton` component displays a social login button which will open
+ * a popup window containing the specified path.
+ *
+ * ### Props
+ *
+* - `path`
+ */
+export default class LogInButton extends Button {
+ static initProps(props) {
+ props.className = (props.className || '') + ' LogInButton';
+
+ props.onclick = function() {
+ const width = 620;
+ const height = 400;
+ const $window = $(window);
+
+ window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup',
+ `width=${width},` +
+ `height=${height},` +
+ `top=${$window.height() / 2 - height / 2},` +
+ `left=${$window.width() / 2 - width / 2},` +
+ 'status=no,scrollbars=no,resizable=no');
+ };
+
+ super.initProps(props);
+ }
+}
diff --git a/framework/core/js/forum/src/components/LogInButtons.js b/framework/core/js/forum/src/components/LogInButtons.js
new file mode 100644
index 000000000..ef2c853e9
--- /dev/null
+++ b/framework/core/js/forum/src/components/LogInButtons.js
@@ -0,0 +1,25 @@
+import Component from 'flarum/Component';
+import ItemList from 'flarum/utils/ItemList';
+
+/**
+ * The `LogInButtons` component displays a collection of social login buttons.
+ */
+export default class LogInButtons extends Component {
+ view() {
+ return (
+
+ {this.items().toArray()}
+
+ );
+ }
+
+ /**
+ * Build a list of LogInButton components.
+ *
+ * @return {ItemList}
+ * @public
+ */
+ items() {
+ return new ItemList();
+ }
+}
diff --git a/framework/core/js/forum/src/components/LogInModal.js b/framework/core/js/forum/src/components/LogInModal.js
index ad839a863..ad2ac1769 100644
--- a/framework/core/js/forum/src/components/LogInModal.js
+++ b/framework/core/js/forum/src/components/LogInModal.js
@@ -3,6 +3,7 @@ import ForgotPasswordModal from 'flarum/components/ForgotPasswordModal';
import SignUpModal from 'flarum/components/SignUpModal';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
+import LogInButtons from 'flarum/components/LogInButtons';
/**
* The `LogInModal` component displays a modal dialog with a login form.
@@ -42,6 +43,8 @@ export default class LogInModal extends Modal {
content() {
return [
+
+
{app.trans('core.forgot_password_link')}
+
{app.forum.attribute('allowSignUp') ? (
{app.trans('core.before_sign_up_link')}{' '}
@@ -84,6 +88,8 @@ export default class LogInModal extends Modal {
/**
* Open the forgot password modal, prefilling it with an email if the user has
* entered one.
+ *
+ * @public
*/
forgotPassword() {
const email = this.email();
@@ -95,6 +101,8 @@ export default class LogInModal extends Modal {
/**
* Open the sign up modal, prefilling it with an email/username/password if
* the user has entered one.
+ *
+ * @public
*/
signUp() {
const props = {password: this.password()};
diff --git a/framework/core/js/forum/src/components/SignUpModal.js b/framework/core/js/forum/src/components/SignUpModal.js
index 0cbdb231e..3d6ffb1df 100644
--- a/framework/core/js/forum/src/components/SignUpModal.js
+++ b/framework/core/js/forum/src/components/SignUpModal.js
@@ -2,6 +2,7 @@ import Modal from 'flarum/components/Modal';
import LogInModal from 'flarum/components/LogInModal';
import avatar from 'flarum/helpers/avatar';
import Button from 'flarum/components/Button';
+import LogInButtons from 'flarum/components/LogInButtons';
/**
* The `SignUpModal` component displays a modal dialog with a singup form.
@@ -11,6 +12,7 @@ import Button from 'flarum/components/Button';
* - `username`
* - `email`
* - `password`
+ * - `token` An email token to sign up with.
*/
export default class SignUpModal extends Modal {
constructor(...args) {
@@ -65,7 +67,9 @@ export default class SignUpModal extends Modal {
}
body() {
- const body = [(
+ const body = [
+ this.props.token ? '' : ,
+
- )];
+ ];
if (this.welcomeUser) {
const user = this.welcomeUser;
@@ -115,20 +121,12 @@ export default class SignUpModal extends Modal {
{avatar(user)}
{app.trans('core.welcome_user', {user})}
- {!user.isActivated() ? [
-
{app.trans('core.confirmation_email_sent', {email: {user.email()}})}
,
-
-
- {app.trans('core.go_to', {location: emailProviderName})}
-
-
- ] : (
-
-
-
- )}
+
{app.trans('core.confirmation_email_sent', {email: {user.email()}})}
,
+
+
+ {app.trans('core.go_to', {location: emailProviderName})}
+
+
@@ -150,6 +148,8 @@ export default class SignUpModal extends Modal {
/**
* Open the log in modal, prefilling it with an email/username/password if
* the user has entered one.
+ *
+ * @public
*/
logIn() {
const props = {
@@ -161,7 +161,7 @@ export default class SignUpModal extends Modal {
}
onready() {
- if (this.props.username) {
+ if (this.props.username && !this.props.token) {
this.$('[name=email]').select();
} else {
super.onready();
@@ -175,24 +175,50 @@ export default class SignUpModal extends Modal {
const data = this.submitData();
- app.store.createRecord('users').save(data).then(
- user => {
- this.welcomeUser = user;
- this.loading = false;
- m.redraw();
+ app.request({
+ url: app.forum.attribute('baseUrl') + '/register',
+ method: 'POST',
+ data
+ }).then(
+ payload => {
+ const user = app.store.pushPayload(payload);
+
+ // If the user's new account has been activated, then we can assume
+ // that they have been logged in too. Thus, we will reload the page.
+ // Otherwise, we will show a message asking them to check their email.
+ if (user.isActivated()) {
+ window.location.reload();
+ } else {
+ this.welcomeUser = user;
+ this.loading = false;
+ m.redraw();
+ }
},
response => {
this.loading = false;
- this.handleErrors(response.errors);
+ this.handleErrors(response);
}
);
}
+ /**
+ * Get the data that should be submitted in the sign-up request.
+ *
+ * @return {Object}
+ * @public
+ */
submitData() {
- return {
+ const data = {
username: this.username(),
- email: this.email(),
- password: this.password()
+ email: this.email()
};
+
+ if (this.props.token) {
+ data.token = this.props.token;
+ } else {
+ data.password = this.password();
+ }
+
+ return data;
}
}
diff --git a/framework/core/js/lib/components/Button.js b/framework/core/js/lib/components/Button.js
index a2ae2d7de..eed25c372 100644
--- a/framework/core/js/lib/components/Button.js
+++ b/framework/core/js/lib/components/Button.js
@@ -25,7 +25,8 @@ export default class Button extends Component {
delete attrs.children;
- attrs.className = (attrs.className || '');
+ attrs.className = attrs.className || '';
+ attrs.type = attrs.type || 'button';
const iconName = extract(attrs, 'icon');
if (iconName) attrs.className += ' hasIcon';
diff --git a/framework/core/less/forum/LogInButton.less b/framework/core/less/forum/LogInButton.less
new file mode 100644
index 000000000..f0fd6c1e9
--- /dev/null
+++ b/framework/core/less/forum/LogInButton.less
@@ -0,0 +1,11 @@
+.LogInButton {
+ &:extend(.Button--block);
+}
+.LogInButtons {
+ width: 200px;
+ margin: 0 auto 20px;
+
+ .LogInButton {
+ margin-bottom: 5px;
+ }
+}
diff --git a/framework/core/less/forum/app.less b/framework/core/less/forum/app.less
index 40bac2b0a..d9b4a3122 100644
--- a/framework/core/less/forum/app.less
+++ b/framework/core/less/forum/app.less
@@ -10,6 +10,7 @@
@import "EditUserModal.less";
@import "Hero.less";
@import "IndexPage.less";
+@import "LogInButton.less";
@import "LogInModal.less";
@import "NotificationGrid.less";
@import "NotificationList.less";
diff --git a/framework/core/locale/en.yml b/framework/core/locale/en.yml
index e68aac89a..c40cdc9e7 100644
--- a/framework/core/locale/en.yml
+++ b/framework/core/locale/en.yml
@@ -28,7 +28,6 @@ core:
discussion_started: "Started {ago} by {username}"
discussion_title: Discussion Title
discussions: Discussions
- dismiss: Dismiss
edit: Edit
editing_post: "Post #{number} in {discussion}"
email: Email
diff --git a/framework/core/migrations/2015_02_24_000000_create_email_tokens_table.php b/framework/core/migrations/2015_02_24_000000_create_email_tokens_table.php
index e14bff848..a199c3e03 100644
--- a/framework/core/migrations/2015_02_24_000000_create_email_tokens_table.php
+++ b/framework/core/migrations/2015_02_24_000000_create_email_tokens_table.php
@@ -23,8 +23,8 @@ class CreateEmailTokensTable extends Migration
{
$this->schema->create('email_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
- $table->integer('user_id')->unsigned();
$table->string('email', 150);
+ $table->integer('user_id')->unsigned();
$table->timestamp('created_at');
});
}
diff --git a/framework/core/migrations/2015_09_15_000000_make_email_tokens_user_id_column_nullable.php b/framework/core/migrations/2015_09_15_000000_make_email_tokens_user_id_column_nullable.php
new file mode 100644
index 000000000..5e8ced5e1
--- /dev/null
+++ b/framework/core/migrations/2015_09_15_000000_make_email_tokens_user_id_column_nullable.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Flarum\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+
+class MakeEmailTokensUserIdColumnNullable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ $this->schema->table('email_tokens', function (Blueprint $table) {
+ $table->integer('user_id')->unsigned()->nullable()->change();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ $this->schema->table('email_tokens', function (Blueprint $table) {
+ $table->integer('user_id')->unsigned()->change();
+ });
+ }
+}
diff --git a/framework/core/src/Admin/Middleware/LoginWithCookieAndCheckAdmin.php b/framework/core/src/Admin/Middleware/LoginWithCookieAndCheckAdmin.php
index b1b0d6d72..04169ca3f 100644
--- a/framework/core/src/Admin/Middleware/LoginWithCookieAndCheckAdmin.php
+++ b/framework/core/src/Admin/Middleware/LoginWithCookieAndCheckAdmin.php
@@ -15,36 +15,32 @@ use Illuminate\Contracts\Container\Container;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Stratigility\MiddlewareInterface;
+use Flarum\Forum\Middleware\LoginWithCookie;
+use Flarum\Core\Exceptions\PermissionDeniedException;
-class LoginWithCookieAndCheckAdmin implements MiddlewareInterface
+class LoginWithCookieAndCheckAdmin extends LoginWithCookie
{
- /**
- * @var Container
- */
- protected $app;
-
- /**
- * @param Container $app
- */
- public function __construct(Container $app)
- {
- $this->app = $app;
- }
-
/**
* {@inheritdoc}
*/
public function __invoke(Request $request, Response $response, callable $out = null)
{
- if (($token = array_get($request->getCookieParams(), 'flarum_remember')) &&
- ($accessToken = AccessToken::valid($token)) &&
- $accessToken->user->isAdmin()
- ) {
- $this->app->instance('flarum.actor', $accessToken->user);
- } else {
- die('Access Denied');
+ if (! $this->logIn($request)) {
+ throw new PermissionDeniedException;
}
return $out ? $out($request, $response) : $response;
}
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getToken(Request $request)
+ {
+ $token = parent::getToken($request);
+
+ if ($token && $token->user && $token->user->isAdmin()) {
+ return $token;
+ }
+ }
}
diff --git a/framework/core/src/Api/AccessToken.php b/framework/core/src/Api/AccessToken.php
index fdad6f4ec..9a6efa56e 100644
--- a/framework/core/src/Api/AccessToken.php
+++ b/framework/core/src/Api/AccessToken.php
@@ -12,6 +12,7 @@ namespace Flarum\Api;
use Flarum\Core\Model;
use DateTime;
+use Exception;
/**
* @todo document database columns with @property
@@ -55,14 +56,13 @@ class AccessToken extends Model
}
/**
- * Get the given token only if it is valid.
+ * Check that the token has not expired.
*
- * @param string $token
- * @return static|null
+ * @return bool
*/
- public static function valid($token)
+ public function isValid()
{
- return static::where('id', $token)->where('expires_at', '>', new DateTime)->first();
+ return $this->expires_at > new DateTime;
}
/**
diff --git a/framework/core/src/Api/Actions/Users/CreateAction.php b/framework/core/src/Api/Actions/Users/CreateAction.php
index d88117f2f..92b85730e 100644
--- a/framework/core/src/Api/Actions/Users/CreateAction.php
+++ b/framework/core/src/Api/Actions/Users/CreateAction.php
@@ -73,8 +73,12 @@ class CreateAction extends BaseCreateAction
*/
protected function create(JsonApiRequest $request)
{
- return $this->bus->dispatch(
+ $user = $this->bus->dispatch(
new RegisterUser($request->actor, $request->get('data'))
);
+
+ $request->actor = $user;
+
+ return $user;
}
}
diff --git a/framework/core/src/Api/Middleware/LoginWithHeader.php b/framework/core/src/Api/Middleware/LoginWithHeader.php
index 1a66456af..c5873bb40 100644
--- a/framework/core/src/Api/Middleware/LoginWithHeader.php
+++ b/framework/core/src/Api/Middleware/LoginWithHeader.php
@@ -50,7 +50,7 @@ class LoginWithHeader implements MiddlewareInterface
if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) {
$token = substr($parts[0], strlen($this->prefix));
- if ($accessToken = AccessToken::valid($token)) {
+ if (($accessToken = AccessToken::find($token)) && $accessToken->isValid()) {
$this->app->instance('flarum.actor', $user = $accessToken->user);
$user->updateLastSeen()->save();
diff --git a/framework/core/src/Core/Users/Commands/ConfirmEmailHandler.php b/framework/core/src/Core/Users/Commands/ConfirmEmailHandler.php
index 71f766915..9825f8926 100644
--- a/framework/core/src/Core/Users/Commands/ConfirmEmailHandler.php
+++ b/framework/core/src/Core/Users/Commands/ConfirmEmailHandler.php
@@ -13,7 +13,6 @@ namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\UserRepository;
use Flarum\Events\UserWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
-use Flarum\Core\Exceptions\InvalidConfirmationTokenException;
use Flarum\Core\Users\EmailToken;
use DateTime;
@@ -36,16 +35,14 @@ class ConfirmEmailHandler
/**
* @param ConfirmEmail $command
- * @return \Flarum\Core\Users\User
+ *
* @throws InvalidConfirmationTokenException
+ *
+ * @return \Flarum\Core\Users\User
*/
public function handle(ConfirmEmail $command)
{
- $token = EmailToken::find($command->token);
-
- if (! $token || $token->created_at < new DateTime('-1 day')) {
- throw new InvalidConfirmationTokenException;
- }
+ $token = EmailToken::validOrFail($command->token);
$user = $token->user;
$user->changeEmail($token->email);
diff --git a/framework/core/src/Core/Users/Commands/RegisterUserHandler.php b/framework/core/src/Core/Users/Commands/RegisterUserHandler.php
index b68a5ea34..132a400e0 100644
--- a/framework/core/src/Core/Users/Commands/RegisterUserHandler.php
+++ b/framework/core/src/Core/Users/Commands/RegisterUserHandler.php
@@ -11,17 +11,25 @@
namespace Flarum\Core\Users\Commands;
use Flarum\Core\Users\User;
+use Flarum\Core\Users\EmailToken;
use Flarum\Events\UserWillBeSaved;
use Flarum\Core\Support\DispatchesEvents;
use Flarum\Core\Settings\SettingsRepository;
use Flarum\Core\Exceptions\PermissionDeniedException;
+use DateTime;
class RegisterUserHandler
{
use DispatchesEvents;
+ /**
+ * @var SettingsRepository
+ */
protected $settings;
+ /**
+ * @param SettingsRepository $settings
+ */
public function __construct(SettingsRepository $settings)
{
$this->settings = $settings;
@@ -29,27 +37,57 @@ class RegisterUserHandler
/**
* @param RegisterUser $command
+ *
+ * @throws PermissionDeniedException if signup is closed and the actor is
+ * not an administrator.
+ * @throws \Flarum\Core\Exceptions\InvalidConfirmationTokenException if an
+ * email confirmation token is provided but is invalid.
+ *
* @return User
- * @throws PermissionDeniedException
*/
public function handle(RegisterUser $command)
{
- if (! $this->settings->get('allow_sign_up')) {
- throw new PermissionDeniedException;
- }
-
$actor = $command->actor;
$data = $command->data;
+ if (! $this->settings->get('allow_sign_up') && ! $actor->isAdmin()) {
+ throw new PermissionDeniedException;
+ }
+
+ // If a valid email confirmation token was provided as an attribute,
+ // then we can create a random password for this user and consider their
+ // email address confirmed.
+ if (isset($data['attributes']['token'])) {
+ $token = EmailToken::whereNull('user_id')->validOrFail($data['attributes']['token']);
+
+ $email = $token->email;
+ $password = array_get($data, 'attributes.password', str_random(20));
+ } else {
+ $email = array_get($data, 'attributes.email');
+ $password = array_get($data, 'attributes.password');
+ }
+
+ // Create the user's new account. If their email was set via token, then
+ // we can activate their account from the get-go, and they won't need
+ // to confirm their email address.
$user = User::register(
array_get($data, 'attributes.username'),
- array_get($data, 'attributes.email'),
- array_get($data, 'attributes.password')
+ $email,
+ $password
);
+ if (isset($token)) {
+ $user->activate();
+ }
+
event(new UserWillBeSaved($user, $actor, $data));
$user->save();
+
+ if (isset($token)) {
+ $token->delete();
+ }
+
$this->dispatchEventsFor($user);
return $user;
diff --git a/framework/core/src/Core/Users/EmailToken.php b/framework/core/src/Core/Users/EmailToken.php
index 92acd1bc9..eb61b0cd0 100644
--- a/framework/core/src/Core/Users/EmailToken.php
+++ b/framework/core/src/Core/Users/EmailToken.php
@@ -11,6 +11,8 @@
namespace Flarum\Core\Users;
use Flarum\Core\Model;
+use Flarum\Core\Exceptions\InvalidConfirmationTokenException;
+use DateTime;
/**
* @todo document database columns with @property
@@ -37,11 +39,12 @@ class EmailToken extends Model
/**
* Generate an email token for the specified user.
*
- * @param int $userId
* @param string $email
+ * @param int $userId
+ *
* @return static
*/
- public static function generate($userId, $email)
+ public static function generate($email, $userId = null)
{
$token = new static;
@@ -62,4 +65,25 @@ class EmailToken extends Model
{
return $this->belongsTo('Flarum\Core\Users\User');
}
+
+ /**
+ * Find the token with the given ID, and assert that it has not expired.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param string $id
+ *
+ * @throws InvalidConfirmationTokenException
+ *
+ * @return static
+ */
+ public function scopeValidOrFail($query, $id)
+ {
+ $token = $query->find($id);
+
+ if (! $token || $token->created_at < new DateTime('-1 day')) {
+ throw new InvalidConfirmationTokenException;
+ }
+
+ return $token;
+ }
}
diff --git a/framework/core/src/Core/Users/Listeners/EmailConfirmationMailer.php b/framework/core/src/Core/Users/Listeners/EmailConfirmationMailer.php
index 497285e3a..7ca9eb384 100755
--- a/framework/core/src/Core/Users/Listeners/EmailConfirmationMailer.php
+++ b/framework/core/src/Core/Users/Listeners/EmailConfirmationMailer.php
@@ -57,6 +57,11 @@ class EmailConfirmationMailer
public function whenUserWasRegistered(UserWasRegistered $event)
{
$user = $event->user;
+
+ if ($user->is_activated) {
+ return;
+ }
+
$data = $this->getEmailData($user, $user->email);
$this->mailer->send(['text' => 'flarum::emails.activateAccount'], $data, function (Message $message) use ($user) {
@@ -82,11 +87,12 @@ class EmailConfirmationMailer
/**
* @param User $user
* @param string $email
+ *
* @return EmailToken
*/
protected function generateToken(User $user, $email)
{
- $token = EmailToken::generate($user->id, $email);
+ $token = EmailToken::generate($email, $user->id);
$token->save();
return $token;
@@ -97,6 +103,7 @@ class EmailConfirmationMailer
*
* @param User $user
* @param string $email
+ *
* @return array
*/
protected function getEmailData(User $user, $email)
diff --git a/framework/core/src/Forum/Actions/ClientAction.php b/framework/core/src/Forum/Actions/ClientAction.php
index bf358d1a3..49224baf6 100644
--- a/framework/core/src/Forum/Actions/ClientAction.php
+++ b/framework/core/src/Forum/Actions/ClientAction.php
@@ -56,7 +56,6 @@ class ClientAction extends BaseClientAction
'core.discussion_started',
'core.discussion_title',
'core.discussions',
- 'core.dismiss',
'core.edit',
'core.editing_post',
'core.email',
diff --git a/framework/core/src/Forum/Actions/ExternalAuthenticatorTrait.php b/framework/core/src/Forum/Actions/ExternalAuthenticatorTrait.php
new file mode 100644
index 000000000..0ed0337ef
--- /dev/null
+++ b/framework/core/src/Forum/Actions/ExternalAuthenticatorTrait.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\Forum\Actions;
+
+use Flarum\Core\Users\User;
+use Zend\Diactoros\Response\HtmlResponse;
+use Flarum\Api\Commands\GenerateAccessToken;
+use Flarum\Core\Users\EmailToken;
+
+trait ExternalAuthenticatorTrait
+{
+ use WritesRememberCookie;
+
+ /**
+ * @var \Illuminate\Contracts\Bus\Dispatcher
+ */
+ protected $bus;
+
+ /**
+ * Respond with JavaScript to tell the Flarum app that the user has been
+ * authenticated, or with information about their sign up status.
+ *
+ * @param string $email The email of the user's account.
+ * @param string $username A suggested username for the user's account.
+ * @return HtmlResponse
+ */
+ protected function authenticated($email, $username)
+ {
+ $user = User::where('email', $email)->first();
+
+ // If a user with this email address doesn't already exist, then we will
+ // generate a unique confirmation token for this email address and add
+ // it to the response, along with the email address and a suggested
+ // username. Otherwise, we will log in the existing user by generating
+ // an access token.
+ if (! $user) {
+ $token = EmailToken::generate($email);
+ $token->save();
+
+ $payload = compact('email', 'username');
+
+ $payload['token'] = $token->id;
+ } else {
+ $accessToken = $this->bus->dispatch(new GenerateAccessToken($user->id));
+
+ $payload = ['authenticated' => true];
+ }
+
+ $content = sprintf('', json_encode($payload));
+
+ $response = new HtmlResponse($content);
+
+ if (isset($accessToken)) {
+ $response = $this->withRememberCookie($response, $accessToken->id);
+ }
+
+ return $response;
+ }
+}
diff --git a/framework/core/src/Forum/Actions/LoginAction.php b/framework/core/src/Forum/Actions/LoginAction.php
index 847c0c5a0..d089df799 100644
--- a/framework/core/src/Forum/Actions/LoginAction.php
+++ b/framework/core/src/Forum/Actions/LoginAction.php
@@ -47,7 +47,7 @@ class LoginAction extends Action
/**
* @param Request $request
* @param array $routeParams
- * @return \Psr\Http\Message\ResponseInterface|EmptyResponse
+ * @return JsonResponse|EmptyResponse
*/
public function handle(Request $request, array $routeParams = [])
{
diff --git a/framework/core/src/Forum/Actions/RegisterAction.php b/framework/core/src/Forum/Actions/RegisterAction.php
new file mode 100644
index 000000000..db9d74e9e
--- /dev/null
+++ b/framework/core/src/Forum/Actions/RegisterAction.php
@@ -0,0 +1,80 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\Forum\Actions;
+
+use Flarum\Api\Client;
+use Flarum\Api\AccessToken;
+use Flarum\Events\UserLoggedIn;
+use Flarum\Support\Action;
+use Flarum\Api\Commands\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 RegisterAction extends Action
+{
+ use WritesRememberCookie;
+
+ /**
+ * @var Dispatcher
+ */
+ protected $bus;
+
+ /**
+ * @var Client
+ */
+ protected $apiClient;
+
+ /**
+ * @param Dispatcher $bus
+ * @param Client $apiClient
+ */
+ public function __construct(Dispatcher $bus, Client $apiClient)
+ {
+ $this->bus = $bus;
+ $this->apiClient = $apiClient;
+ }
+
+ /**
+ * @param Request $request
+ * @param array $routeParams
+ *
+ * @return JsonResponse
+ */
+ public function handle(Request $request, array $routeParams = [])
+ {
+ $params = ['data' => ['attributes' => $request->getAttributes()]];
+
+ $apiResponse = $this->apiClient->send(app('flarum.actor'), 'Flarum\Api\Actions\Users\CreateAction', $params);
+
+ $body = $apiResponse->getBody();
+ $statusCode = $apiResponse->getStatusCode();
+
+ $response = new JsonResponse($body, $statusCode);
+
+ 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
+ );
+ }
+
+ return $response;
+ }
+}
diff --git a/framework/core/src/Forum/ForumServiceProvider.php b/framework/core/src/Forum/ForumServiceProvider.php
index af6d98acf..0edd323e6 100644
--- a/framework/core/src/Forum/ForumServiceProvider.php
+++ b/framework/core/src/Forum/ForumServiceProvider.php
@@ -102,6 +102,12 @@ class ForumServiceProvider extends ServiceProvider
$this->action('Flarum\Forum\Actions\LoginAction')
);
+ $routes->post(
+ '/register',
+ 'flarum.forum.register',
+ $this->action('Flarum\Forum\Actions\RegisterAction')
+ );
+
$routes->get(
'/confirm/{token}',
'flarum.forum.confirmEmail',
diff --git a/framework/core/src/Forum/Middleware/LoginWithCookie.php b/framework/core/src/Forum/Middleware/LoginWithCookie.php
index 95e5e9605..78ce75879 100644
--- a/framework/core/src/Forum/Middleware/LoginWithCookie.php
+++ b/framework/core/src/Forum/Middleware/LoginWithCookie.php
@@ -36,14 +36,46 @@ class LoginWithCookie implements MiddlewareInterface
*/
public function __invoke(Request $request, Response $response, callable $out = null)
{
- if (($token = array_get($request->getCookieParams(), 'flarum_remember')) &&
- ($accessToken = AccessToken::valid($token))
- ) {
- $this->app->instance('flarum.actor', $user = $accessToken->user);
-
- $user->updateLastSeen()->save();
- }
+ $this->logIn($request);
return $out ? $out($request, $response) : $response;
}
+
+ /**
+ * Set the application's actor instance according to the request token.
+ *
+ * @param Request $request
+ * @return bool
+ */
+ protected function logIn(Request $request)
+ {
+ if ($token = $this->getToken($request)) {
+ if (! $token->isValid()) {
+ // TODO: https://github.com/flarum/core/issues/253
+ } elseif ($token->user) {
+ $this->app->instance('flarum.actor', $user = $token->user);
+
+ $user->updateLastSeen()->save();
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 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');
+
+ if ($token) {
+ return AccessToken::find($token);
+ }
+ }
}