mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-23 19:32:29 +01:00
Finished new user invite flow
This commit is contained in:
parent
e5155a5dcb
commit
42d8548960
106
app/Http/Controllers/Auth/UserInviteController.php
Normal file
106
app/Http/Controllers/Auth/UserInviteController.php
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use BookStack\Auth\Access\UserInviteService;
|
||||||
|
use BookStack\Auth\UserRepo;
|
||||||
|
use BookStack\Exceptions\UserTokenExpiredException;
|
||||||
|
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||||
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Contracts\View\Factory;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Redirector;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class UserInviteController extends Controller
|
||||||
|
{
|
||||||
|
protected $inviteService;
|
||||||
|
protected $userRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @param UserInviteService $inviteService
|
||||||
|
* @param UserRepo $userRepo
|
||||||
|
*/
|
||||||
|
public function __construct(UserInviteService $inviteService, UserRepo $userRepo)
|
||||||
|
{
|
||||||
|
$this->inviteService = $inviteService;
|
||||||
|
$this->userRepo = $userRepo;
|
||||||
|
$this->middleware('guest');
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the page for the user to set the password for their account.
|
||||||
|
* @param string $token
|
||||||
|
* @return Factory|View|RedirectResponse
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function showSetPassword(string $token)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->inviteService->checkTokenAndGetUserId($token);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
return $this->handleTokenException($exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('auth.invite-set-password', [
|
||||||
|
'token' => $token,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the password for an invited user and then grants them access.
|
||||||
|
* @param string $token
|
||||||
|
* @param Request $request
|
||||||
|
* @return RedirectResponse|Redirector
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function setPassword(string $token, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'password' => 'required|min:6'
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userId = $this->inviteService->checkTokenAndGetUserId($token);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
return $this->handleTokenException($exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->userRepo->getById($userId);
|
||||||
|
$user->password = bcrypt($request->get('password'));
|
||||||
|
$user->email_confirmed = true;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
auth()->login($user);
|
||||||
|
session()->flash('success', trans('auth.user_invite_success', ['appName' => setting('app-name')]));
|
||||||
|
$this->inviteService->deleteByUser($user);
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and validate the exception thrown when checking an invite token.
|
||||||
|
* @param Exception $exception
|
||||||
|
* @return RedirectResponse|Redirector
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function handleTokenException(Exception $exception)
|
||||||
|
{
|
||||||
|
if ($exception instanceof UserTokenNotFoundException) {
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exception instanceof UserTokenExpiredException) {
|
||||||
|
session()->flash('error', trans('errors.invite_token_expired'));
|
||||||
|
return redirect('/password/email');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\SocialAuthService;
|
use BookStack\Auth\Access\SocialAuthService;
|
||||||
|
use BookStack\Auth\Access\UserInviteService;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
use BookStack\Exceptions\UserUpdateException;
|
use BookStack\Exceptions\UserUpdateException;
|
||||||
@ -13,18 +14,21 @@ class UserController extends Controller
|
|||||||
|
|
||||||
protected $user;
|
protected $user;
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
|
protected $inviteService;
|
||||||
protected $imageRepo;
|
protected $imageRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserController constructor.
|
* UserController constructor.
|
||||||
* @param User $user
|
* @param User $user
|
||||||
* @param UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
|
* @param UserInviteService $inviteService
|
||||||
* @param ImageRepo $imageRepo
|
* @param ImageRepo $imageRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
|
public function __construct(User $user, UserRepo $userRepo, UserInviteService $inviteService, ImageRepo $imageRepo)
|
||||||
{
|
{
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
|
$this->inviteService = $inviteService;
|
||||||
$this->imageRepo = $imageRepo;
|
$this->imageRepo = $imageRepo;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@ -75,8 +79,10 @@ class UserController extends Controller
|
|||||||
];
|
];
|
||||||
|
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
if ($authMethod === 'standard') {
|
$sendInvite = ($request->get('send_invite', 'false') === 'true');
|
||||||
$validationRules['password'] = 'required|min:5';
|
|
||||||
|
if ($authMethod === 'standard' && !$sendInvite) {
|
||||||
|
$validationRules['password'] = 'required|min:6';
|
||||||
$validationRules['password-confirm'] = 'required|same:password';
|
$validationRules['password-confirm'] = 'required|same:password';
|
||||||
} elseif ($authMethod === 'ldap') {
|
} elseif ($authMethod === 'ldap') {
|
||||||
$validationRules['external_auth_id'] = 'required';
|
$validationRules['external_auth_id'] = 'required';
|
||||||
@ -86,13 +92,17 @@ class UserController extends Controller
|
|||||||
$user = $this->user->fill($request->all());
|
$user = $this->user->fill($request->all());
|
||||||
|
|
||||||
if ($authMethod === 'standard') {
|
if ($authMethod === 'standard') {
|
||||||
$user->password = bcrypt($request->get('password'));
|
$user->password = bcrypt($request->get('password', str_random(32)));
|
||||||
} elseif ($authMethod === 'ldap') {
|
} elseif ($authMethod === 'ldap') {
|
||||||
$user->external_auth_id = $request->get('external_auth_id');
|
$user->external_auth_id = $request->get('external_auth_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
|
if ($sendInvite) {
|
||||||
|
$this->inviteService->sendInvitation($user);
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->filled('roles')) {
|
if ($request->filled('roles')) {
|
||||||
$roles = $request->get('roles');
|
$roles = $request->get('roles');
|
||||||
$this->userRepo->setUserRoles($user, $roles);
|
$this->userRepo->setUserRoles($user, $roles);
|
||||||
@ -139,7 +149,7 @@ class UserController extends Controller
|
|||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'min:2',
|
'name' => 'min:2',
|
||||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||||
'password' => 'min:5|required_with:password_confirm',
|
'password' => 'min:6|required_with:password_confirm',
|
||||||
'password-confirm' => 'same:password|required_with:password',
|
'password-confirm' => 'same:password|required_with:password',
|
||||||
'setting' => 'array',
|
'setting' => 'array',
|
||||||
'profile_image' => $this->imageRepo->getImageValidationRules(),
|
'profile_image' => $this->imageRepo->getImageValidationRules(),
|
||||||
|
@ -28,6 +28,7 @@ import bookSort from "./book-sort";
|
|||||||
import settingAppColorPicker from "./setting-app-color-picker";
|
import settingAppColorPicker from "./setting-app-color-picker";
|
||||||
import entityPermissionsEditor from "./entity-permissions-editor";
|
import entityPermissionsEditor from "./entity-permissions-editor";
|
||||||
import templateManager from "./template-manager";
|
import templateManager from "./template-manager";
|
||||||
|
import newUserPassword from "./new-user-password";
|
||||||
|
|
||||||
const componentMapping = {
|
const componentMapping = {
|
||||||
'dropdown': dropdown,
|
'dropdown': dropdown,
|
||||||
@ -60,6 +61,7 @@ const componentMapping = {
|
|||||||
'setting-app-color-picker': settingAppColorPicker,
|
'setting-app-color-picker': settingAppColorPicker,
|
||||||
'entity-permissions-editor': entityPermissionsEditor,
|
'entity-permissions-editor': entityPermissionsEditor,
|
||||||
'template-manager': templateManager,
|
'template-manager': templateManager,
|
||||||
|
'new-user-password': newUserPassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.components = {};
|
window.components = {};
|
||||||
|
28
resources/assets/js/components/new-user-password.js
Normal file
28
resources/assets/js/components/new-user-password.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
class NewUserPassword {
|
||||||
|
|
||||||
|
constructor(elem) {
|
||||||
|
this.elem = elem;
|
||||||
|
this.inviteOption = elem.querySelector('input[name=send_invite]');
|
||||||
|
|
||||||
|
if (this.inviteOption) {
|
||||||
|
this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
|
||||||
|
this.inviteOptionChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteOptionChange() {
|
||||||
|
const inviting = (this.inviteOption.value === 'true');
|
||||||
|
const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
|
||||||
|
for (const input of passwordBoxes) {
|
||||||
|
input.disabled = inviting;
|
||||||
|
}
|
||||||
|
const container = this.elem.querySelector('#password-input-container');
|
||||||
|
if (container) {
|
||||||
|
container.style.display = inviting ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewUserPassword;
|
@ -11,6 +11,11 @@ class ToggleSwitch {
|
|||||||
|
|
||||||
stateChange() {
|
stateChange() {
|
||||||
this.input.value = (this.checkbox.checked ? 'true' : 'false');
|
this.input.value = (this.checkbox.checked ? 'true' : 'false');
|
||||||
|
|
||||||
|
// Dispatch change event from hidden input so they can be listened to
|
||||||
|
// like a normal checkbox.
|
||||||
|
const changeEvent = new Event('change');
|
||||||
|
this.input.dispatchEvent(changeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,11 @@ return [
|
|||||||
|
|
||||||
// User Invite
|
// User Invite
|
||||||
'user_invite_email_subject' => 'You have been invited to join :appName!',
|
'user_invite_email_subject' => 'You have been invited to join :appName!',
|
||||||
'user_invite_email_greeting' => 'A user account has been created for you on :appName.',
|
'user_invite_email_greeting' => 'An account has been created for you on :appName.',
|
||||||
'user_invite_email_text' => 'Click the button below to set an account password and gain access:',
|
'user_invite_email_text' => 'Click the button below to set an account password and gain access:',
|
||||||
'user_invite_email_action' => 'Set Account Password',
|
'user_invite_email_action' => 'Set Account Password',
|
||||||
|
'user_invite_page_welcome' => 'Welcome to :appName!',
|
||||||
|
'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',
|
||||||
|
'user_invite_page_confirm_button' => 'Confirm Password',
|
||||||
|
'user_invite_success' => 'Password set, you now have access to :appName!'
|
||||||
];
|
];
|
@ -27,7 +27,7 @@ return [
|
|||||||
'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
|
'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
|
||||||
'social_driver_not_found' => 'Social driver not found',
|
'social_driver_not_found' => 'Social driver not found',
|
||||||
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
|
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
|
||||||
'invite_token_expired' => 'This invitation link has expired. You can try to reset your account password or request a new invite from an administrator.',
|
'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
|
||||||
|
|
||||||
// System
|
// System
|
||||||
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
|
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
|
||||||
|
@ -109,7 +109,9 @@ return [
|
|||||||
'users_role' => 'User Roles',
|
'users_role' => 'User Roles',
|
||||||
'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
|
'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
|
||||||
'users_password' => 'User Password',
|
'users_password' => 'User Password',
|
||||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 5 characters long.',
|
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.',
|
||||||
|
'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
|
||||||
|
'users_send_invite_option' => 'Send user invite email',
|
||||||
'users_external_auth_id' => 'External Authentication ID',
|
'users_external_auth_id' => 'External Authentication ID',
|
||||||
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your LDAP system.',
|
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your LDAP system.',
|
||||||
'users_password_warning' => 'Only fill the below if you would like to change your password.',
|
'users_password_warning' => 'Only fill the below if you would like to change your password.',
|
||||||
|
27
resources/views/auth/invite-set-password.blade.php
Normal file
27
resources/views/auth/invite-set-password.blade.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@extends('simple-layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="container very-small mt-xl">
|
||||||
|
<div class="card content-wrap auto-height">
|
||||||
|
<h1 class="list-heading">{{ trans('auth.user_invite_page_welcome', ['appName' => setting('app-name')]) }}</h1>
|
||||||
|
<p>{{ trans('auth.user_invite_page_text', ['appName' => setting('app-name')]) }}</p>
|
||||||
|
|
||||||
|
<form action="{{ url('/register/invite/' . $token) }}" method="POST" class="stretch-inputs">
|
||||||
|
{!! csrf_field() !!}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">{{ trans('auth.password') }}</label>
|
||||||
|
@include('form.password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<button class="button primary">{{ trans('auth.user_invite_page_confirm_button') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@stop
|
@ -48,23 +48,40 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($authMethod === 'standard')
|
@if($authMethod === 'standard')
|
||||||
<div>
|
<div new-user-password>
|
||||||
<label class="setting-list-label">{{ trans('settings.users_password') }}</label>
|
<label class="setting-list-label">{{ trans('settings.users_password') }}</label>
|
||||||
<p class="small">{{ trans('settings.users_password_desc') }}</p>
|
|
||||||
@if(isset($model))
|
@if(!isset($model))
|
||||||
<p class="small">
|
<p class="small">
|
||||||
{{ trans('settings.users_password_warning') }}
|
{{ trans('settings.users_send_invite_text') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@include('components.toggle-switch', [
|
||||||
|
'name' => 'send_invite',
|
||||||
|
'value' => old('send_invite', 'true') === 'true',
|
||||||
|
'label' => trans('settings.users_send_invite_option')
|
||||||
|
])
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
<div class="grid half mt-m gap-xl">
|
|
||||||
<div>
|
<div id="password-input-container" @if(!isset($model)) style="display: none;" @endif>
|
||||||
<label for="password">{{ trans('auth.password') }}</label>
|
<p class="small">{{ trans('settings.users_password_desc') }}</p>
|
||||||
@include('form.password', ['name' => 'password'])
|
@if(isset($model))
|
||||||
</div>
|
<p class="small">
|
||||||
<div>
|
{{ trans('settings.users_password_warning') }}
|
||||||
<label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
|
</p>
|
||||||
@include('form.password', ['name' => 'password-confirm'])
|
@endif
|
||||||
|
<div class="grid half mt-m gap-xl">
|
||||||
|
<div>
|
||||||
|
<label for="password">{{ trans('auth.password') }}</label>
|
||||||
|
@include('form.password', ['name' => 'password'])
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
|
||||||
|
@include('form.password', ['name' => 'password-confirm'])
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
@ -217,6 +217,10 @@ Route::post('/register/confirm/resend', 'Auth\ConfirmEmailController@resend');
|
|||||||
Route::get('/register/confirm/{token}', 'Auth\ConfirmEmailController@confirm');
|
Route::get('/register/confirm/{token}', 'Auth\ConfirmEmailController@confirm');
|
||||||
Route::post('/register', 'Auth\RegisterController@postRegister');
|
Route::post('/register', 'Auth\RegisterController@postRegister');
|
||||||
|
|
||||||
|
// User invitation routes
|
||||||
|
Route::get('/register/invite/{token}', 'Auth\UserInviteController@showSetPassword');
|
||||||
|
Route::post('/register/invite/{token}', 'Auth\UserInviteController@setPassword');
|
||||||
|
|
||||||
// Password reset link request routes...
|
// Password reset link request routes...
|
||||||
Route::get('/password/email', 'Auth\ForgotPasswordController@showLinkRequestForm');
|
Route::get('/password/email', 'Auth\ForgotPasswordController@showLinkRequestForm');
|
||||||
Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
|
Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
|
||||||
|
Loading…
Reference in New Issue
Block a user