diff --git a/framework/core/composer.json b/framework/core/composer.json
index ff06ace99..151719c8e 100644
--- a/framework/core/composer.json
+++ b/framework/core/composer.json
@@ -42,7 +42,6 @@
"illuminate/view": "5.5.*",
"intervention/image": "^2.3.0",
"league/flysystem": "^1.0.11",
- "league/oauth2-client": "~1.0",
"matthiasmullie/minify": "^1.3",
"middlewares/base-path": "^1.1",
"middlewares/base-path-router": "^0.2.1",
diff --git a/framework/core/js/src/forum/ForumApplication.js b/framework/core/js/src/forum/ForumApplication.js
index 2e52abb1d..4a1439ead 100644
--- a/framework/core/js/src/forum/ForumApplication.js
+++ b/framework/core/js/src/forum/ForumApplication.js
@@ -148,17 +148,16 @@ export default class ForumApplication extends Application {
* 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
+ * modal. A truthy `loggedIn` prop indicates that the user has logged
* in, and thus the page is reloaded.
* @public
*/
authenticationComplete(payload) {
- if (payload.authenticated) {
+ if (payload.loggedIn) {
window.location.reload();
} else {
const modal = new SignUpModal(payload);
this.modal.show(modal);
- modal.$('[name=password]').focus();
}
}
}
diff --git a/framework/core/js/src/forum/components/SignUpModal.js b/framework/core/js/src/forum/components/SignUpModal.js
index b651e29aa..94ac1e162 100644
--- a/framework/core/js/src/forum/components/SignUpModal.js
+++ b/framework/core/js/src/forum/components/SignUpModal.js
@@ -42,7 +42,7 @@ export default class SignUpModal extends Modal {
}
className() {
- return 'Modal--small SignUpModal' + (this.welcomeUser ? ' SignUpModal--success' : '');
+ return 'Modal--small SignUpModal';
}
title() {
@@ -61,7 +61,7 @@ export default class SignUpModal extends Modal {
}
isProvided(field) {
- return this.props.identificationFields && this.props.identificationFields.indexOf(field) !== -1;
+ return this.props.provided && this.props.provided.indexOf(field) !== -1;
}
body() {
@@ -179,10 +179,6 @@ export default class SignUpModal extends Modal {
data.password = this.password();
}
- if (this.props.avatarUrl) {
- data.avatarUrl = this.props.avatarUrl;
- }
-
return data;
}
}
diff --git a/framework/core/migrations/2018_09_22_004100_change_registration_tokens_columns.php b/framework/core/migrations/2018_09_22_004100_change_registration_tokens_columns.php
new file mode 100644
index 000000000..526899c84
--- /dev/null
+++ b/framework/core/migrations/2018_09_22_004100_change_registration_tokens_columns.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+
+return [
+ 'up' => function (Builder $schema) {
+ $schema->table('registration_tokens', function (Blueprint $table) {
+ $table->string('provider');
+ $table->string('identifier');
+ $table->text('user_attributes')->nullable();
+
+ $table->text('payload')->nullable()->change();
+ });
+ },
+
+ 'down' => function (Builder $schema) {
+ $schema->table('auth_tokens', function (Blueprint $table) {
+ $table->dropColumn('provider', 'identifier', 'user_attributes');
+
+ $table->string('payload', 150)->change();
+ });
+ }
+];
diff --git a/framework/core/migrations/2018_09_22_004200_create_login_providers_table.php b/framework/core/migrations/2018_09_22_004200_create_login_providers_table.php
new file mode 100644
index 000000000..3d395830e
--- /dev/null
+++ b/framework/core/migrations/2018_09_22_004200_create_login_providers_table.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Flarum\Database\Migration;
+use Illuminate\Database\Schema\Blueprint;
+
+return Migration::createTable(
+ 'login_providers',
+ function (Blueprint $table) {
+ $table->increments('id');
+ $table->unsignedInteger('user_id');
+ $table->string('provider', 100);
+ $table->string('identifier', 100);
+ $table->dateTime('created_at')->nullable();
+ $table->dateTime('last_login_at')->nullable();
+
+ $table->unique(['provider', 'identifier']);
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ }
+);
diff --git a/framework/core/src/Forum/Auth/Registration.php b/framework/core/src/Forum/Auth/Registration.php
new file mode 100644
index 000000000..46ac0d6ad
--- /dev/null
+++ b/framework/core/src/Forum/Auth/Registration.php
@@ -0,0 +1,127 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\Forum\Auth;
+
+class Registration
+{
+ /**
+ * @var array
+ */
+ protected $provided = [];
+
+ /**
+ * @var array
+ */
+ protected $suggested = [];
+
+ /**
+ * @var mixed
+ */
+ protected $payload;
+
+ /**
+ * @return array
+ */
+ public function getProvided(): array
+ {
+ return $this->provided;
+ }
+
+ /**
+ * @return array
+ */
+ public function getSuggested(): array
+ {
+ return $this->suggested;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getPayload()
+ {
+ return $this->payload;
+ }
+
+ /**
+ * @param string $key
+ * @param mixed $value
+ * @return $this
+ */
+ public function provide(string $key, $value): self
+ {
+ $this->provided[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $email
+ * @return $this
+ */
+ public function provideTrustedEmail(string $email): self
+ {
+ return $this->provide('email', $email);
+ }
+
+ /**
+ * @param string $url
+ * @return $this
+ */
+ public function provideAvatar(string $url): self
+ {
+ return $this->provide('avatar_url', $url);
+ }
+
+ /**
+ * @param string $key
+ * @param mixed $value
+ * @return $this
+ */
+ public function suggest(string $key, $value): self
+ {
+ $this->suggested[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $username
+ * @return $this
+ */
+ public function suggestUsername(string $username): self
+ {
+ $username = preg_replace('/[^a-z0-9-_]/i', '', $username);
+
+ return $this->suggest('username', $username);
+ }
+
+ /**
+ * @param string $email
+ * @return $this
+ */
+ public function suggestEmail(string $email): self
+ {
+ return $this->suggest('email', $email);
+ }
+
+ /**
+ * @param mixed $payload
+ * @return $this
+ */
+ public function setPayload($payload): self
+ {
+ $this->payload = $payload;
+
+ return $this;
+ }
+}
diff --git a/framework/core/src/Forum/Auth/ResponseFactory.php b/framework/core/src/Forum/Auth/ResponseFactory.php
new file mode 100644
index 000000000..37eb5e9cc
--- /dev/null
+++ b/framework/core/src/Forum/Auth/ResponseFactory.php
@@ -0,0 +1,81 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\Forum\Auth;
+
+use Flarum\Http\Rememberer;
+use Flarum\User\LoginProvider;
+use Flarum\User\RegistrationToken;
+use Flarum\User\User;
+use Psr\Http\Message\ResponseInterface;
+use Zend\Diactoros\Response\HtmlResponse;
+
+class ResponseFactory
+{
+ /**
+ * @var Rememberer
+ */
+ protected $rememberer;
+
+ /**
+ * @param Rememberer $rememberer
+ */
+ public function __construct(Rememberer $rememberer)
+ {
+ $this->rememberer = $rememberer;
+ }
+
+ public function make(string $provider, string $identifier, callable $configureRegistration): ResponseInterface
+ {
+ if ($user = LoginProvider::logIn($provider, $identifier)) {
+ return $this->makeLoggedInResponse($user);
+ }
+
+ $configureRegistration($registration = new Registration);
+
+ $provided = $registration->getProvided();
+
+ if (! empty($provided['email']) && $user = User::where(array_only($provided, 'email'))->first()) {
+ $user->loginProviders()->create(compact('provider', 'identifier'));
+
+ return $this->makeLoggedInResponse($user);
+ }
+
+ $token = RegistrationToken::generate($provider, $identifier, $provided, $registration->getPayload());
+ $token->save();
+
+ return $this->makeResponse(array_merge(
+ $provided,
+ $registration->getSuggested(),
+ [
+ 'token' => $token->id,
+ 'provided' => array_keys($provided)
+ ]
+ ));
+ }
+
+ private function makeResponse(array $payload): HtmlResponse
+ {
+ $content = sprintf(
+ '',
+ json_encode($payload)
+ );
+
+ return new HtmlResponse($content);
+ }
+
+ private function makeLoggedInResponse(User $user)
+ {
+ $response = $this->makeResponse(['loggedIn' => true]);
+
+ return $this->rememberer->rememberUser($response, $user->id);
+ }
+}
diff --git a/framework/core/src/Forum/AuthenticationResponseFactory.php b/framework/core/src/Forum/AuthenticationResponseFactory.php
deleted file mode 100644
index 6a17c8c2d..000000000
--- a/framework/core/src/Forum/AuthenticationResponseFactory.php
+++ /dev/null
@@ -1,118 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Flarum\Forum;
-
-use Flarum\Http\Rememberer;
-use Flarum\Http\SessionAuthenticator;
-use Flarum\User\AuthToken;
-use Flarum\User\User;
-use Psr\Http\Message\ServerRequestInterface as Request;
-use Zend\Diactoros\Response\HtmlResponse;
-
-class AuthenticationResponseFactory
-{
- /**
- * @var SessionAuthenticator
- */
- protected $authenticator;
-
- /**
- * @var Rememberer
- */
- protected $rememberer;
-
- /**
- * AuthenticationResponseFactory constructor.
- * @param SessionAuthenticator $authenticator
- * @param Rememberer $rememberer
- */
- public function __construct(SessionAuthenticator $authenticator, Rememberer $rememberer)
- {
- $this->authenticator = $authenticator;
- $this->rememberer = $rememberer;
- }
-
- public function make(Request $request, array $identification, array $suggestions = [])
- {
- if (isset($suggestions['username'])) {
- $suggestions['username'] = $this->sanitizeUsername($suggestions['username']);
- }
-
- $user = User::where($identification)->first();
-
- $payload = $this->getPayload($identification, $suggestions, $user);
-
- $response = $this->getResponse($payload);
-
- if ($user) {
- $session = $request->getAttribute('session');
- $this->authenticator->logIn($session, $user->id);
-
- $response = $this->rememberer->rememberUser($response, $user->id);
- }
-
- return $response;
- }
-
- /**
- * @param string $username
- * @return string
- */
- private function sanitizeUsername($username)
- {
- return preg_replace('/[^a-z0-9-_]/i', '', $username);
- }
-
- /**
- * @param array $payload
- * @return HtmlResponse
- */
- private function getResponse(array $payload)
- {
- $content = sprintf(
- '',
- json_encode($payload)
- );
-
- return new HtmlResponse($content);
- }
-
- /**
- * @param array $identification
- * @param array $suggestions
- * @param User|null $user
- * @return array
- */
- private function getPayload(array $identification, array $suggestions, User $user = null)
- {
- // If a user with these attributes already exists, then we will log them
- // in by generating an access token. Otherwise, we will generate a
- // unique token for these attributes and add it to the response, along
- // with the suggested account information.
- if ($user) {
- $payload = ['authenticated' => true];
- } else {
- $token = AuthToken::generate($identification);
- $token->save();
-
- $payload = array_merge(
- $identification,
- $suggestions,
- ['token' => $token->id],
- // List of the fields that can't be edited during sign up
- ['identificationFields' => array_keys($identification)]
- );
- }
-
- return $payload;
- }
-}
diff --git a/framework/core/src/Forum/Controller/AbstractOAuth2Controller.php b/framework/core/src/Forum/Controller/AbstractOAuth2Controller.php
deleted file mode 100644
index bd6522f64..000000000
--- a/framework/core/src/Forum/Controller/AbstractOAuth2Controller.php
+++ /dev/null
@@ -1,107 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Flarum\Forum\Controller;
-
-use Flarum\Forum\AuthenticationResponseFactory;
-use League\OAuth2\Client\Provider\ResourceOwnerInterface;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface as Request;
-use Psr\Http\Server\RequestHandlerInterface;
-use Zend\Diactoros\Response\RedirectResponse;
-
-abstract class AbstractOAuth2Controller implements RequestHandlerInterface
-{
- /**
- * @var AuthenticationResponseFactory
- */
- protected $authResponse;
-
- /**
- * @var \League\OAuth2\Client\Provider\AbstractProvider
- */
- protected $provider;
-
- /**
- * The access token, once obtained.
- *
- * @var string
- */
- protected $token;
-
- /**
- * @param AuthenticationResponseFactory $authResponse
- */
- public function __construct(AuthenticationResponseFactory $authResponse)
- {
- $this->authResponse = $authResponse;
- }
-
- /**
- * @param Request $request
- * @return ResponseInterface
- */
- public function handle(Request $request): ResponseInterface
- {
- $redirectUri = (string) $request->getAttribute('originalUri', $request->getUri())->withQuery('');
-
- $this->provider = $this->getProvider($redirectUri);
-
- $session = $request->getAttribute('session');
-
- $queryParams = $request->getQueryParams();
- $code = array_get($queryParams, 'code');
- $state = array_get($queryParams, 'state');
-
- if (! $code) {
- $authUrl = $this->provider->getAuthorizationUrl($this->getAuthorizationUrlOptions());
- $session->put('oauth2state', $this->provider->getState());
-
- return new RedirectResponse($authUrl.'&display=popup');
- } elseif (! $state || $state !== $session->get('oauth2state')) {
- $session->remove('oauth2state');
- echo 'Invalid state. Please close the window and try again.';
- exit;
- }
-
- $this->token = $this->provider->getAccessToken('authorization_code', compact('code'));
-
- $owner = $this->provider->getResourceOwner($this->token);
-
- $identification = $this->getIdentification($owner);
- $suggestions = $this->getSuggestions($owner);
-
- return $this->authResponse->make($request, $identification, $suggestions);
- }
-
- /**
- * @param string $redirectUri
- * @return \League\OAuth2\Client\Provider\AbstractProvider
- */
- abstract protected function getProvider($redirectUri);
-
- /**
- * @return array
- */
- abstract protected function getAuthorizationUrlOptions();
-
- /**
- * @param ResourceOwnerInterface $resourceOwner
- * @return array
- */
- abstract protected function getIdentification(ResourceOwnerInterface $resourceOwner);
-
- /**
- * @param ResourceOwnerInterface $resourceOwner
- * @return array
- */
- abstract protected function getSuggestions(ResourceOwnerInterface $resourceOwner);
-}
diff --git a/framework/core/src/Http/Middleware/CollectGarbage.php b/framework/core/src/Http/Middleware/CollectGarbage.php
index 9b0da016c..3160a6aca 100644
--- a/framework/core/src/Http/Middleware/CollectGarbage.php
+++ b/framework/core/src/Http/Middleware/CollectGarbage.php
@@ -13,9 +13,9 @@ namespace Flarum\Http\Middleware;
use Carbon\Carbon;
use Flarum\Http\AccessToken;
-use Flarum\User\AuthToken;
use Flarum\User\EmailToken;
use Flarum\User\PasswordToken;
+use Flarum\User\RegistrationToken;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
@@ -64,7 +64,7 @@ class CollectGarbage implements Middleware
EmailToken::where('created_at', '<=', $earliestToKeep)->delete();
PasswordToken::where('created_at', '<=', $earliestToKeep)->delete();
- AuthToken::where('created_at', '<=', $earliestToKeep)->delete();
+ RegistrationToken::where('created_at', '<=', $earliestToKeep)->delete();
$this->sessionHandler->gc($this->getSessionLifetimeInSeconds());
}
diff --git a/framework/core/src/User/Command/EditUserHandler.php b/framework/core/src/User/Command/EditUserHandler.php
index 6d9d2485d..33261ca60 100644
--- a/framework/core/src/User/Command/EditUserHandler.php
+++ b/framework/core/src/User/Command/EditUserHandler.php
@@ -11,19 +11,15 @@
namespace Flarum\User\Command;
-use Exception;
use Flarum\Foundation\DispatchEventsTrait;
use Flarum\User\AssertPermissionTrait;
-use Flarum\User\AvatarUploader;
use Flarum\User\Event\GroupsChanged;
use Flarum\User\Event\Saving;
use Flarum\User\User;
use Flarum\User\UserRepository;
use Flarum\User\UserValidator;
use Illuminate\Contracts\Events\Dispatcher;
-use Illuminate\Contracts\Validation\Factory;
use Illuminate\Validation\ValidationException;
-use Intervention\Image\ImageManager;
class EditUserHandler
{
@@ -40,36 +36,23 @@ class EditUserHandler
*/
protected $validator;
- /**
- * @var AvatarUploader
- */
- protected $avatarUploader;
-
- /**
- * @var Factory
- */
- private $validatorFactory;
-
/**
* @param Dispatcher $events
* @param \Flarum\User\UserRepository $users
* @param UserValidator $validator
- * @param AvatarUploader $avatarUploader
- * @param Factory $validatorFactory
*/
- public function __construct(Dispatcher $events, UserRepository $users, UserValidator $validator, AvatarUploader $avatarUploader, Factory $validatorFactory)
+ public function __construct(Dispatcher $events, UserRepository $users, UserValidator $validator)
{
$this->events = $events;
$this->users = $users;
$this->validator = $validator;
- $this->avatarUploader = $avatarUploader;
- $this->validatorFactory = $validatorFactory;
}
/**
* @param EditUser $command
* @return User
* @throws \Flarum\User\Exception\PermissionDeniedException
+ * @throws ValidationException
*/
public function handle(EditUser $command)
{
@@ -146,28 +129,6 @@ class EditUserHandler
});
}
- if ($avatarUrl = array_get($attributes, 'avatarUrl')) {
- $this->assertPermission($canEdit);
-
- $validation = $this->validatorFactory->make(compact('avatarUrl'), ['avatarUrl' => 'url']);
-
- if ($validation->fails()) {
- throw new ValidationException($validation);
- }
-
- try {
- $image = (new ImageManager)->make($avatarUrl);
-
- $this->avatarUploader->upload($user, $image);
- } catch (Exception $e) {
- //
- }
- } elseif (array_key_exists('avatarUrl', $attributes)) {
- $this->assertPermission($canEdit);
-
- $this->avatarUploader->remove($user);
- }
-
$this->events->dispatch(
new Saving($user, $actor, $data)
);
diff --git a/framework/core/src/User/Command/RegisterUserHandler.php b/framework/core/src/User/Command/RegisterUserHandler.php
index 05029bb70..a8899a55d 100644
--- a/framework/core/src/User/Command/RegisterUserHandler.php
+++ b/framework/core/src/User/Command/RegisterUserHandler.php
@@ -11,18 +11,17 @@
namespace Flarum\User\Command;
-use Exception;
use Flarum\Foundation\DispatchEventsTrait;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
-use Flarum\User\AuthToken;
use Flarum\User\AvatarUploader;
+use Flarum\User\Event\RegisteringFromProvider;
use Flarum\User\Event\Saving;
use Flarum\User\Exception\PermissionDeniedException;
+use Flarum\User\RegistrationToken;
use Flarum\User\User;
use Flarum\User\UserValidator;
use Illuminate\Contracts\Events\Dispatcher;
-use Illuminate\Contracts\Validation\Factory;
use Illuminate\Validation\ValidationException;
use Intervention\Image\ImageManager;
@@ -46,34 +45,26 @@ class RegisterUserHandler
*/
protected $avatarUploader;
- /**
- * @var Factory
- */
- private $validatorFactory;
-
/**
* @param Dispatcher $events
* @param SettingsRepositoryInterface $settings
* @param UserValidator $validator
* @param AvatarUploader $avatarUploader
- * @param Factory $validatorFactory
*/
- public function __construct(Dispatcher $events, SettingsRepositoryInterface $settings, UserValidator $validator, AvatarUploader $avatarUploader, Factory $validatorFactory)
+ public function __construct(Dispatcher $events, SettingsRepositoryInterface $settings, UserValidator $validator, AvatarUploader $avatarUploader)
{
$this->events = $events;
$this->settings = $settings;
$this->validator = $validator;
$this->avatarUploader = $avatarUploader;
- $this->validatorFactory = $validatorFactory;
}
/**
* @param RegisterUser $command
+ * @return User
* @throws PermissionDeniedException if signup is closed and the actor is
* not an administrator.
- * @throws \Flarum\User\Exception\InvalidConfirmationTokenException if an
- * email confirmation token is provided but is invalid.
- * @return User
+ * @throws ValidationException
*/
public function handle(RegisterUser $command)
{
@@ -84,32 +75,24 @@ class RegisterUserHandler
$this->assertAdmin($actor);
}
- $username = array_get($data, 'attributes.username');
- $email = array_get($data, 'attributes.email');
$password = array_get($data, 'attributes.password');
// If a valid authentication token was provided as an attribute,
// then we won't require the user to choose a password.
if (isset($data['attributes']['token'])) {
- $token = AuthToken::validOrFail($data['attributes']['token']);
+ $token = RegistrationToken::validOrFail($data['attributes']['token']);
$password = $password ?: str_random(20);
}
- $user = User::register($username, $email, $password);
+ $user = User::register(
+ array_get($data, 'attributes.username'),
+ array_get($data, 'attributes.email'),
+ $password
+ );
- // If a valid authentication token was provided, then we will assign
- // the attributes associated with it to the user's account. If this
- // includes an email address, then we will activate the user's account
- // from the get-go.
if (isset($token)) {
- foreach ($token->payload as $k => $v) {
- $user->$k = $v;
- }
-
- if (isset($token->payload['email'])) {
- $user->activate();
- }
+ $this->applyToken($user, $token);
}
if ($actor->isAdmin() && array_get($data, 'attributes.isEmailConfirmed')) {
@@ -122,30 +105,53 @@ class RegisterUserHandler
$this->validator->assertValid(array_merge($user->getAttributes(), compact('password')));
- if ($avatarUrl = array_get($data, 'attributes.avatarUrl')) {
- $validation = $this->validatorFactory->make(compact('avatarUrl'), ['avatarUrl' => 'url']);
-
- if ($validation->fails()) {
- throw new ValidationException($validation);
- }
-
- try {
- $image = (new ImageManager)->make($avatarUrl);
-
- $this->avatarUploader->upload($user, $image);
- } catch (Exception $e) {
- //
- }
- }
-
$user->save();
if (isset($token)) {
- $token->delete();
+ $this->fulfillToken($user, $token);
}
$this->dispatchEventsFor($user, $actor);
return $user;
}
+
+ private function applyToken(User $user, RegistrationToken $token)
+ {
+ foreach ($token->user_attributes as $k => $v) {
+ if ($k === 'avatar_url') {
+ $this->uploadAvatarFromUrl($user, $v);
+ continue;
+ }
+
+ $user->$k = $v;
+
+ if ($k === 'email') {
+ $user->activate();
+ }
+ }
+
+ $this->events->dispatch(
+ new RegisteringFromProvider($user, $token->provider, $token->payload)
+ );
+ }
+
+ private function uploadAvatarFromUrl(User $user, string $url)
+ {
+ $image = (new ImageManager)->make($url);
+
+ $this->avatarUploader->upload($user, $image);
+ }
+
+ private function fulfillToken(User $user, RegistrationToken $token)
+ {
+ $token->delete();
+
+ if ($token->provider && $token->identifier) {
+ $user->loginProviders()->create([
+ 'provider' => $token->provider,
+ 'identifier' => $token->identifier
+ ]);
+ }
+ }
}
diff --git a/framework/core/src/User/Event/RegisteringFromProvider.php b/framework/core/src/User/Event/RegisteringFromProvider.php
new file mode 100644
index 000000000..6b96a3bff
--- /dev/null
+++ b/framework/core/src/User/Event/RegisteringFromProvider.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\User\Event;
+
+use Flarum\User\User;
+
+class RegisteringFromProvider
+{
+ /**
+ * @var User
+ */
+ public $user;
+
+ /**
+ * @var string
+ */
+ public $provider;
+
+ /**
+ * @var array
+ */
+ public $payload;
+
+ /**
+ * @param User $user
+ * @param $provider
+ * @param $payload
+ */
+ public function __construct(User $user, string $provider, array $payload)
+ {
+ $this->user = $user;
+ $this->provider = $provider;
+ $this->payload = $payload;
+ }
+}
diff --git a/framework/core/src/User/LoginProvider.php b/framework/core/src/User/LoginProvider.php
new file mode 100644
index 000000000..e0fb1bf8c
--- /dev/null
+++ b/framework/core/src/User/LoginProvider.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Flarum\User;
+
+use Flarum\Database\AbstractModel;
+
+/**
+ * @property int $id
+ * @property int $user_id
+ * @property string $provider
+ * @property string $identifier
+ * @property \Illuminate\Support\Carbon $created_at
+ * @property \Illuminate\Support\Carbon $last_login_at
+ * @property-read User $user
+ */
+class LoginProvider extends AbstractModel
+{
+ protected $dates = ['created_at', 'last_login_at'];
+
+ public $timestamps = true;
+
+ const UPDATED_AT = 'last_login_at';
+
+ protected $fillable = ['provider', 'identifier'];
+
+ /**
+ * Get the user that the login provider belongs to.
+ */
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+
+ /**
+ * Get the user associated with the provider so that they can be logged in.
+ *
+ * @param string $provider
+ * @param string $identifier
+ * @return User|null
+ */
+ public static function logIn(string $provider, string $identifier): ?User
+ {
+ if ($provider = static::where(compact('provider', 'identifier'))->first()) {
+ $provider->touch();
+
+ return $provider->user;
+ }
+
+ return null;
+ }
+}
diff --git a/framework/core/src/User/AuthToken.php b/framework/core/src/User/RegistrationToken.php
similarity index 64%
rename from framework/core/src/User/AuthToken.php
rename to framework/core/src/User/RegistrationToken.php
index 877cd7c24..61c6a5144 100644
--- a/framework/core/src/User/AuthToken.php
+++ b/framework/core/src/User/RegistrationToken.php
@@ -17,16 +17,14 @@ use Flarum\User\Exception\InvalidConfirmationTokenException;
/**
* @property string $token
+ * @property string $provider
+ * @property string $identifier
+ * @property array $user_attributes
+ * @property array $payload
* @property \Carbon\Carbon $created_at
- * @property string $payload
*/
-class AuthToken extends AbstractModel
+class RegistrationToken extends AbstractModel
{
- /**
- * {@inheritdoc}
- */
- protected $table = 'registration_tokens';
-
/**
* The attributes that should be mutated to dates.
*
@@ -34,6 +32,11 @@ class AuthToken extends AbstractModel
*/
protected $dates = ['created_at'];
+ protected $casts = [
+ 'user_attributes' => 'array',
+ 'payload' => 'array'
+ ];
+
/**
* Use a custom primary key for this model.
*
@@ -47,44 +50,28 @@ class AuthToken extends AbstractModel
protected $primaryKey = 'token';
/**
- * Generate an email token for the specified user.
- *
- * @param string $payload
+ * Generate an auth token for the specified user.
*
+ * @param string $provider
+ * @param string $identifier
+ * @param array $attributes
+ * @param array $payload
* @return static
*/
- public static function generate($payload)
+ public static function generate(string $provider, string $identifier, array $attributes, array $payload)
{
$token = new static;
$token->token = str_random(40);
+ $token->provider = $provider;
+ $token->identifier = $identifier;
+ $token->user_attributes = $attributes;
$token->payload = $payload;
$token->created_at = Carbon::now();
return $token;
}
- /**
- * Unserialize the payload attribute from the database's JSON value.
- *
- * @param string $value
- * @return string
- */
- public function getPayloadAttribute($value)
- {
- return json_decode($value, true);
- }
-
- /**
- * Serialize the payload attribute to be stored in the database as JSON.
- *
- * @param string $value
- */
- public function setPayloadAttribute($value)
- {
- $this->attributes['payload'] = json_encode($value);
- }
-
/**
* Find the token with the given ID, and assert that it has not expired.
*
@@ -93,11 +80,11 @@ class AuthToken extends AbstractModel
*
* @throws InvalidConfirmationTokenException
*
- * @return AuthToken
+ * @return RegistrationToken
*/
public function scopeValidOrFail($query, string $token)
{
- /** @var AuthToken $token */
+ /** @var RegistrationToken $token */
$token = $query->find($token);
if (! $token || $token->created_at->lessThan(Carbon::now()->subDay())) {
diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php
index b4742e212..cd3b7c035 100644
--- a/framework/core/src/User/User.php
+++ b/framework/core/src/User/User.php
@@ -668,6 +668,14 @@ class User extends AbstractModel
return $this->hasMany('Flarum\Http\AccessToken');
}
+ /**
+ * Get the user's login providers.
+ */
+ public function loginProviders()
+ {
+ return $this->hasMany(LoginProvider::class);
+ }
+
/**
* @param string $ability
* @param array|mixed $arguments