mirror of
https://github.com/flarum/framework.git
synced 2024-12-05 09:03:36 +08:00
Add external authenticator (social login) API
Allows registrations to be completed with a pre-confirmed email address and no password.
This commit is contained in:
parent
9f13e454f0
commit
1059147e6c
|
@ -44,6 +44,7 @@ export default class AppearancePage extends Component {
|
|||
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
type: 'submit',
|
||||
children: 'Save Changes',
|
||||
loading: this.loading
|
||||
})}
|
||||
|
|
|
@ -30,6 +30,7 @@ export default class EditCustomCssModal extends Modal {
|
|||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
type: 'submit',
|
||||
children: 'Save Changes',
|
||||
loading: this.loading
|
||||
})}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
30
framework/core/js/forum/src/components/LogInButton.js
Normal file
30
framework/core/js/forum/src/components/LogInButton.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
25
framework/core/js/forum/src/components/LogInButtons.js
Normal file
25
framework/core/js/forum/src/components/LogInButtons.js
Normal file
|
@ -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 (
|
||||
<div className="LogInButtons">
|
||||
{this.items().toArray()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a list of LogInButton components.
|
||||
*
|
||||
* @return {ItemList}
|
||||
* @public
|
||||
*/
|
||||
items() {
|
||||
return new ItemList();
|
||||
}
|
||||
}
|
|
@ -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 [
|
||||
<div className="Modal-body">
|
||||
<LogInButtons/>
|
||||
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="email" placeholder={app.trans('core.username_or_email')}
|
||||
|
@ -71,6 +74,7 @@ export default class LogInModal extends Modal {
|
|||
<p className="LogInModal-forgotPassword">
|
||||
<a onclick={this.forgotPassword.bind(this)}>{app.trans('core.forgot_password_link')}</a>
|
||||
</p>
|
||||
|
||||
{app.forum.attribute('allowSignUp') ? (
|
||||
<p className="LogInModal-signUp">
|
||||
{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()};
|
||||
|
|
|
@ -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 ? '' : <LogInButtons/>,
|
||||
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="username" placeholder={app.trans('core.username')}
|
||||
|
@ -78,26 +82,28 @@ export default class SignUpModal extends Modal {
|
|||
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
disabled={this.loading} />
|
||||
disabled={this.loading || this.props.token} />
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)}
|
||||
disabled={this.loading} />
|
||||
</div>
|
||||
{this.props.token ? '' : (
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)}
|
||||
disabled={this.loading} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.trans('core.sign_up')
|
||||
})}
|
||||
<Button
|
||||
className="Button Button--primary Button--block"
|
||||
type="submit"
|
||||
loading={this.loading}>
|
||||
{app.trans('core.sign_up')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)];
|
||||
];
|
||||
|
||||
if (this.welcomeUser) {
|
||||
const user = this.welcomeUser;
|
||||
|
@ -115,20 +121,12 @@ export default class SignUpModal extends Modal {
|
|||
{avatar(user)}
|
||||
<h3>{app.trans('core.welcome_user', {user})}</h3>
|
||||
|
||||
{!user.isActivated() ? [
|
||||
<p>{app.trans('core.confirmation_email_sent', {email: <strong>{user.email()}</strong>})}</p>,
|
||||
<p>
|
||||
<a href={`http://${emailProviderName}`} className="Button Button--primary" target="_blank">
|
||||
{app.trans('core.go_to', {location: emailProviderName})}
|
||||
</a>
|
||||
</p>
|
||||
] : (
|
||||
<p>
|
||||
<button className="Button Button--primary" onclick={this.hide.bind(this)}>
|
||||
{app.trans('core.dismiss')}
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
<p>{app.trans('core.confirmation_email_sent', {email: <strong>{user.email()}</strong>})}</p>,
|
||||
<p>
|
||||
<a href={`http://${emailProviderName}`} className="Button Button--primary" target="_blank">
|
||||
{app.trans('core.go_to', {location: emailProviderName})}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
11
framework/core/less/forum/LogInButton.less
Normal file
11
framework/core/less/forum/LogInButton.less
Normal file
|
@ -0,0 +1,11 @@
|
|||
.LogInButton {
|
||||
&:extend(.Button--block);
|
||||
}
|
||||
.LogInButtons {
|
||||
width: 200px;
|
||||
margin: 0 auto 20px;
|
||||
|
||||
.LogInButton {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<?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\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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
<?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\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('<script>
|
||||
window.opener.app.authenticationComplete(%s);
|
||||
window.close();
|
||||
</script>', json_encode($payload));
|
||||
|
||||
$response = new HtmlResponse($content);
|
||||
|
||||
if (isset($accessToken)) {
|
||||
$response = $this->withRememberCookie($response, $accessToken->id);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
|
@ -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 = [])
|
||||
{
|
||||
|
|
80
framework/core/src/Forum/Actions/RegisterAction.php
Normal file
80
framework/core/src/Forum/Actions/RegisterAction.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?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\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;
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user