mirror of
https://github.com/flarum/framework.git
synced 2025-01-23 06:13:17 +08:00
Auth token and avatarUrl security improvements (#1514)
* Remove AbstractOAuth2Controller There is no reason to provide an implementation for a specific oAuth2 library in core; it's not generic enough (eg. auth-twitter can't use it). This code could be moved into another package which auth extensions depend on, but it's a negligible amount of relatively simple code that I don't think it's worth the trouble. * Introduce login providers Users can have many login providers (a combination of a provider name and an identifier for that user, eg. their Facebook ID). After retrieving user data from a provider (eg. Facebook), you pass the login provider details into the Auth\ResponseFactory. If an associated user is found, a response that logs them in will be returned. If not, a registration token will be created so the user can proceed to sign up. Once the token is fulfilled, the login provider will be associated with the user.
This commit is contained in:
parent
8107d9787c
commit
0c429c1b9f
|
@ -42,7 +42,6 @@
|
||||||
"illuminate/view": "5.5.*",
|
"illuminate/view": "5.5.*",
|
||||||
"intervention/image": "^2.3.0",
|
"intervention/image": "^2.3.0",
|
||||||
"league/flysystem": "^1.0.11",
|
"league/flysystem": "^1.0.11",
|
||||||
"league/oauth2-client": "~1.0",
|
|
||||||
"matthiasmullie/minify": "^1.3",
|
"matthiasmullie/minify": "^1.3",
|
||||||
"middlewares/base-path": "^1.1",
|
"middlewares/base-path": "^1.1",
|
||||||
"middlewares/base-path-router": "^0.2.1",
|
"middlewares/base-path-router": "^0.2.1",
|
||||||
|
|
|
@ -148,17 +148,16 @@ export default class ForumApplication extends Application {
|
||||||
* with the provided details.
|
* with the provided details.
|
||||||
*
|
*
|
||||||
* @param {Object} payload A dictionary of props to pass into the sign up
|
* @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.
|
* in, and thus the page is reloaded.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
authenticationComplete(payload) {
|
authenticationComplete(payload) {
|
||||||
if (payload.authenticated) {
|
if (payload.loggedIn) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
const modal = new SignUpModal(payload);
|
const modal = new SignUpModal(payload);
|
||||||
this.modal.show(modal);
|
this.modal.show(modal);
|
||||||
modal.$('[name=password]').focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default class SignUpModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
return 'Modal--small SignUpModal' + (this.welcomeUser ? ' SignUpModal--success' : '');
|
return 'Modal--small SignUpModal';
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
|
@ -61,7 +61,7 @@ export default class SignUpModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
isProvided(field) {
|
isProvided(field) {
|
||||||
return this.props.identificationFields && this.props.identificationFields.indexOf(field) !== -1;
|
return this.props.provided && this.props.provided.indexOf(field) !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body() {
|
body() {
|
||||||
|
@ -179,10 +179,6 @@ export default class SignUpModal extends Modal {
|
||||||
data.password = this.password();
|
data.password = this.password();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.avatarUrl) {
|
|
||||||
data.avatarUrl = this.props.avatarUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
];
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
);
|
127
framework/core/src/Forum/Auth/Registration.php
Normal file
127
framework/core/src/Forum/Auth/Registration.php
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
<?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\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;
|
||||||
|
}
|
||||||
|
}
|
81
framework/core/src/Forum/Auth/ResponseFactory.php
Normal file
81
framework/core/src/Forum/Auth/ResponseFactory.php
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\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(
|
||||||
|
'<script>window.close(); window.opener.app.authenticationComplete(%s);</script>',
|
||||||
|
json_encode($payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new HtmlResponse($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeLoggedInResponse(User $user)
|
||||||
|
{
|
||||||
|
$response = $this->makeResponse(['loggedIn' => true]);
|
||||||
|
|
||||||
|
return $this->rememberer->rememberUser($response, $user->id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,118 +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;
|
|
||||||
|
|
||||||
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(
|
|
||||||
'<script>window.opener.app.authenticationComplete(%s); window.close();</script>',
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,107 +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 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);
|
|
||||||
}
|
|
|
@ -13,9 +13,9 @@ namespace Flarum\Http\Middleware;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Flarum\Http\AccessToken;
|
use Flarum\Http\AccessToken;
|
||||||
use Flarum\User\AuthToken;
|
|
||||||
use Flarum\User\EmailToken;
|
use Flarum\User\EmailToken;
|
||||||
use Flarum\User\PasswordToken;
|
use Flarum\User\PasswordToken;
|
||||||
|
use Flarum\User\RegistrationToken;
|
||||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||||
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;
|
||||||
|
@ -64,7 +64,7 @@ class CollectGarbage implements Middleware
|
||||||
|
|
||||||
EmailToken::where('created_at', '<=', $earliestToKeep)->delete();
|
EmailToken::where('created_at', '<=', $earliestToKeep)->delete();
|
||||||
PasswordToken::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());
|
$this->sessionHandler->gc($this->getSessionLifetimeInSeconds());
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,19 +11,15 @@
|
||||||
|
|
||||||
namespace Flarum\User\Command;
|
namespace Flarum\User\Command;
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Flarum\Foundation\DispatchEventsTrait;
|
use Flarum\Foundation\DispatchEventsTrait;
|
||||||
use Flarum\User\AssertPermissionTrait;
|
use Flarum\User\AssertPermissionTrait;
|
||||||
use Flarum\User\AvatarUploader;
|
|
||||||
use Flarum\User\Event\GroupsChanged;
|
use Flarum\User\Event\GroupsChanged;
|
||||||
use Flarum\User\Event\Saving;
|
use Flarum\User\Event\Saving;
|
||||||
use Flarum\User\User;
|
use Flarum\User\User;
|
||||||
use Flarum\User\UserRepository;
|
use Flarum\User\UserRepository;
|
||||||
use Flarum\User\UserValidator;
|
use Flarum\User\UserValidator;
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Contracts\Validation\Factory;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Intervention\Image\ImageManager;
|
|
||||||
|
|
||||||
class EditUserHandler
|
class EditUserHandler
|
||||||
{
|
{
|
||||||
|
@ -40,36 +36,23 @@ class EditUserHandler
|
||||||
*/
|
*/
|
||||||
protected $validator;
|
protected $validator;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var AvatarUploader
|
|
||||||
*/
|
|
||||||
protected $avatarUploader;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Factory
|
|
||||||
*/
|
|
||||||
private $validatorFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Dispatcher $events
|
* @param Dispatcher $events
|
||||||
* @param \Flarum\User\UserRepository $users
|
* @param \Flarum\User\UserRepository $users
|
||||||
* @param UserValidator $validator
|
* @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->events = $events;
|
||||||
$this->users = $users;
|
$this->users = $users;
|
||||||
$this->validator = $validator;
|
$this->validator = $validator;
|
||||||
$this->avatarUploader = $avatarUploader;
|
|
||||||
$this->validatorFactory = $validatorFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param EditUser $command
|
* @param EditUser $command
|
||||||
* @return User
|
* @return User
|
||||||
* @throws \Flarum\User\Exception\PermissionDeniedException
|
* @throws \Flarum\User\Exception\PermissionDeniedException
|
||||||
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function handle(EditUser $command)
|
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(
|
$this->events->dispatch(
|
||||||
new Saving($user, $actor, $data)
|
new Saving($user, $actor, $data)
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,18 +11,17 @@
|
||||||
|
|
||||||
namespace Flarum\User\Command;
|
namespace Flarum\User\Command;
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Flarum\Foundation\DispatchEventsTrait;
|
use Flarum\Foundation\DispatchEventsTrait;
|
||||||
use Flarum\Settings\SettingsRepositoryInterface;
|
use Flarum\Settings\SettingsRepositoryInterface;
|
||||||
use Flarum\User\AssertPermissionTrait;
|
use Flarum\User\AssertPermissionTrait;
|
||||||
use Flarum\User\AuthToken;
|
|
||||||
use Flarum\User\AvatarUploader;
|
use Flarum\User\AvatarUploader;
|
||||||
|
use Flarum\User\Event\RegisteringFromProvider;
|
||||||
use Flarum\User\Event\Saving;
|
use Flarum\User\Event\Saving;
|
||||||
use Flarum\User\Exception\PermissionDeniedException;
|
use Flarum\User\Exception\PermissionDeniedException;
|
||||||
|
use Flarum\User\RegistrationToken;
|
||||||
use Flarum\User\User;
|
use Flarum\User\User;
|
||||||
use Flarum\User\UserValidator;
|
use Flarum\User\UserValidator;
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Contracts\Validation\Factory;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
|
|
||||||
|
@ -46,34 +45,26 @@ class RegisterUserHandler
|
||||||
*/
|
*/
|
||||||
protected $avatarUploader;
|
protected $avatarUploader;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Factory
|
|
||||||
*/
|
|
||||||
private $validatorFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Dispatcher $events
|
* @param Dispatcher $events
|
||||||
* @param SettingsRepositoryInterface $settings
|
* @param SettingsRepositoryInterface $settings
|
||||||
* @param UserValidator $validator
|
* @param UserValidator $validator
|
||||||
* @param AvatarUploader $avatarUploader
|
* @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->events = $events;
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
$this->validator = $validator;
|
$this->validator = $validator;
|
||||||
$this->avatarUploader = $avatarUploader;
|
$this->avatarUploader = $avatarUploader;
|
||||||
$this->validatorFactory = $validatorFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param RegisterUser $command
|
* @param RegisterUser $command
|
||||||
|
* @return User
|
||||||
* @throws PermissionDeniedException if signup is closed and the actor is
|
* @throws PermissionDeniedException if signup is closed and the actor is
|
||||||
* not an administrator.
|
* not an administrator.
|
||||||
* @throws \Flarum\User\Exception\InvalidConfirmationTokenException if an
|
* @throws ValidationException
|
||||||
* email confirmation token is provided but is invalid.
|
|
||||||
* @return User
|
|
||||||
*/
|
*/
|
||||||
public function handle(RegisterUser $command)
|
public function handle(RegisterUser $command)
|
||||||
{
|
{
|
||||||
|
@ -84,32 +75,24 @@ class RegisterUserHandler
|
||||||
$this->assertAdmin($actor);
|
$this->assertAdmin($actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
$username = array_get($data, 'attributes.username');
|
|
||||||
$email = array_get($data, 'attributes.email');
|
|
||||||
$password = array_get($data, 'attributes.password');
|
$password = array_get($data, 'attributes.password');
|
||||||
|
|
||||||
// If a valid authentication token was provided as an attribute,
|
// If a valid authentication token was provided as an attribute,
|
||||||
// then we won't require the user to choose a password.
|
// then we won't require the user to choose a password.
|
||||||
if (isset($data['attributes']['token'])) {
|
if (isset($data['attributes']['token'])) {
|
||||||
$token = AuthToken::validOrFail($data['attributes']['token']);
|
$token = RegistrationToken::validOrFail($data['attributes']['token']);
|
||||||
|
|
||||||
$password = $password ?: str_random(20);
|
$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)) {
|
if (isset($token)) {
|
||||||
foreach ($token->payload as $k => $v) {
|
$this->applyToken($user, $token);
|
||||||
$user->$k = $v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($token->payload['email'])) {
|
|
||||||
$user->activate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($actor->isAdmin() && array_get($data, 'attributes.isEmailConfirmed')) {
|
if ($actor->isAdmin() && array_get($data, 'attributes.isEmailConfirmed')) {
|
||||||
|
@ -122,30 +105,53 @@ class RegisterUserHandler
|
||||||
|
|
||||||
$this->validator->assertValid(array_merge($user->getAttributes(), compact('password')));
|
$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();
|
$user->save();
|
||||||
|
|
||||||
if (isset($token)) {
|
if (isset($token)) {
|
||||||
$token->delete();
|
$this->fulfillToken($user, $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->dispatchEventsFor($user, $actor);
|
$this->dispatchEventsFor($user, $actor);
|
||||||
|
|
||||||
return $user;
|
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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
44
framework/core/src/User/Event/RegisteringFromProvider.php
Normal file
44
framework/core/src/User/Event/RegisteringFromProvider.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?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\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;
|
||||||
|
}
|
||||||
|
}
|
60
framework/core/src/User/LoginProvider.php
Normal file
60
framework/core/src/User/LoginProvider.php
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?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\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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,16 +17,14 @@ use Flarum\User\Exception\InvalidConfirmationTokenException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $token
|
* @property string $token
|
||||||
|
* @property string $provider
|
||||||
|
* @property string $identifier
|
||||||
|
* @property array $user_attributes
|
||||||
|
* @property array $payload
|
||||||
* @property \Carbon\Carbon $created_at
|
* @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.
|
* The attributes that should be mutated to dates.
|
||||||
*
|
*
|
||||||
|
@ -34,6 +32,11 @@ class AuthToken extends AbstractModel
|
||||||
*/
|
*/
|
||||||
protected $dates = ['created_at'];
|
protected $dates = ['created_at'];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'user_attributes' => 'array',
|
||||||
|
'payload' => 'array'
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use a custom primary key for this model.
|
* Use a custom primary key for this model.
|
||||||
*
|
*
|
||||||
|
@ -47,44 +50,28 @@ class AuthToken extends AbstractModel
|
||||||
protected $primaryKey = 'token';
|
protected $primaryKey = 'token';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate an email token for the specified user.
|
* Generate an auth token for the specified user.
|
||||||
*
|
|
||||||
* @param string $payload
|
|
||||||
*
|
*
|
||||||
|
* @param string $provider
|
||||||
|
* @param string $identifier
|
||||||
|
* @param array $attributes
|
||||||
|
* @param array $payload
|
||||||
* @return static
|
* @return static
|
||||||
*/
|
*/
|
||||||
public static function generate($payload)
|
public static function generate(string $provider, string $identifier, array $attributes, array $payload)
|
||||||
{
|
{
|
||||||
$token = new static;
|
$token = new static;
|
||||||
|
|
||||||
$token->token = str_random(40);
|
$token->token = str_random(40);
|
||||||
|
$token->provider = $provider;
|
||||||
|
$token->identifier = $identifier;
|
||||||
|
$token->user_attributes = $attributes;
|
||||||
$token->payload = $payload;
|
$token->payload = $payload;
|
||||||
$token->created_at = Carbon::now();
|
$token->created_at = Carbon::now();
|
||||||
|
|
||||||
return $token;
|
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.
|
* Find the token with the given ID, and assert that it has not expired.
|
||||||
*
|
*
|
||||||
|
@ -93,11 +80,11 @@ class AuthToken extends AbstractModel
|
||||||
*
|
*
|
||||||
* @throws InvalidConfirmationTokenException
|
* @throws InvalidConfirmationTokenException
|
||||||
*
|
*
|
||||||
* @return AuthToken
|
* @return RegistrationToken
|
||||||
*/
|
*/
|
||||||
public function scopeValidOrFail($query, string $token)
|
public function scopeValidOrFail($query, string $token)
|
||||||
{
|
{
|
||||||
/** @var AuthToken $token */
|
/** @var RegistrationToken $token */
|
||||||
$token = $query->find($token);
|
$token = $query->find($token);
|
||||||
|
|
||||||
if (! $token || $token->created_at->lessThan(Carbon::now()->subDay())) {
|
if (! $token || $token->created_at->lessThan(Carbon::now()->subDay())) {
|
|
@ -668,6 +668,14 @@ class User extends AbstractModel
|
||||||
return $this->hasMany('Flarum\Http\AccessToken');
|
return $this->hasMany('Flarum\Http\AccessToken');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user's login providers.
|
||||||
|
*/
|
||||||
|
public function loginProviders()
|
||||||
|
{
|
||||||
|
return $this->hasMany(LoginProvider::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $ability
|
* @param string $ability
|
||||||
* @param array|mixed $arguments
|
* @param array|mixed $arguments
|
||||||
|
|
Loading…
Reference in New Issue
Block a user