mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-11-03 02:13:16 +03:00 
			
		
		
		
	Finished new user invite flow
This commit is contained in:
		
							
								
								
									
										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;
 | 
			
		||||
 | 
			
		||||
use BookStack\Auth\Access\SocialAuthService;
 | 
			
		||||
use BookStack\Auth\Access\UserInviteService;
 | 
			
		||||
use BookStack\Auth\User;
 | 
			
		||||
use BookStack\Auth\UserRepo;
 | 
			
		||||
use BookStack\Exceptions\UserUpdateException;
 | 
			
		||||
@@ -13,18 +14,21 @@ class UserController extends Controller
 | 
			
		||||
 | 
			
		||||
    protected $user;
 | 
			
		||||
    protected $userRepo;
 | 
			
		||||
    protected $inviteService;
 | 
			
		||||
    protected $imageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * UserController constructor.
 | 
			
		||||
     * @param User $user
 | 
			
		||||
     * @param UserRepo $userRepo
 | 
			
		||||
     * @param UserInviteService $inviteService
 | 
			
		||||
     * @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->userRepo = $userRepo;
 | 
			
		||||
        $this->inviteService = $inviteService;
 | 
			
		||||
        $this->imageRepo = $imageRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
@@ -75,8 +79,10 @@ class UserController extends Controller
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $authMethod = config('auth.method');
 | 
			
		||||
        if ($authMethod === 'standard') {
 | 
			
		||||
            $validationRules['password'] = 'required|min:5';
 | 
			
		||||
        $sendInvite = ($request->get('send_invite', 'false') === 'true');
 | 
			
		||||
 | 
			
		||||
        if ($authMethod === 'standard' && !$sendInvite) {
 | 
			
		||||
            $validationRules['password'] = 'required|min:6';
 | 
			
		||||
            $validationRules['password-confirm'] = 'required|same:password';
 | 
			
		||||
        } elseif ($authMethod === 'ldap') {
 | 
			
		||||
            $validationRules['external_auth_id'] = 'required';
 | 
			
		||||
@@ -86,13 +92,17 @@ class UserController extends Controller
 | 
			
		||||
        $user = $this->user->fill($request->all());
 | 
			
		||||
 | 
			
		||||
        if ($authMethod === 'standard') {
 | 
			
		||||
            $user->password = bcrypt($request->get('password'));
 | 
			
		||||
            $user->password = bcrypt($request->get('password', str_random(32)));
 | 
			
		||||
        } elseif ($authMethod === 'ldap') {
 | 
			
		||||
            $user->external_auth_id = $request->get('external_auth_id');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $user->save();
 | 
			
		||||
 | 
			
		||||
        if ($sendInvite) {
 | 
			
		||||
            $this->inviteService->sendInvitation($user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($request->filled('roles')) {
 | 
			
		||||
            $roles = $request->get('roles');
 | 
			
		||||
            $this->userRepo->setUserRoles($user, $roles);
 | 
			
		||||
@@ -139,7 +149,7 @@ class UserController extends Controller
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'name'             => 'min:2',
 | 
			
		||||
            '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',
 | 
			
		||||
            'setting'          => 'array',
 | 
			
		||||
            'profile_image'    => $this->imageRepo->getImageValidationRules(),
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ import bookSort from "./book-sort";
 | 
			
		||||
import settingAppColorPicker from "./setting-app-color-picker";
 | 
			
		||||
import entityPermissionsEditor from "./entity-permissions-editor";
 | 
			
		||||
import templateManager from "./template-manager";
 | 
			
		||||
import newUserPassword from "./new-user-password";
 | 
			
		||||
 | 
			
		||||
const componentMapping = {
 | 
			
		||||
    'dropdown': dropdown,
 | 
			
		||||
@@ -60,6 +61,7 @@ const componentMapping = {
 | 
			
		||||
    'setting-app-color-picker': settingAppColorPicker,
 | 
			
		||||
    'entity-permissions-editor': entityPermissionsEditor,
 | 
			
		||||
    'template-manager': templateManager,
 | 
			
		||||
    'new-user-password': newUserPassword,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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() {
 | 
			
		||||
        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_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_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_driver_not_found' => 'Social driver not found',
 | 
			
		||||
    '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
 | 
			
		||||
    '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_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_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_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.',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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,8 +48,23 @@
 | 
			
		||||
@endif
 | 
			
		||||
 | 
			
		||||
@if($authMethod === 'standard')
 | 
			
		||||
    <div>
 | 
			
		||||
    <div new-user-password>
 | 
			
		||||
        <label class="setting-list-label">{{ trans('settings.users_password') }}</label>
 | 
			
		||||
 | 
			
		||||
        @if(!isset($model))
 | 
			
		||||
            <p class="small">
 | 
			
		||||
                {{ trans('settings.users_send_invite_text') }}
 | 
			
		||||
            </p>
 | 
			
		||||
 | 
			
		||||
            @include('components.toggle-switch', [
 | 
			
		||||
                'name' => 'send_invite',
 | 
			
		||||
                'value' => old('send_invite', 'true') === 'true',
 | 
			
		||||
                'label' => trans('settings.users_send_invite_option')
 | 
			
		||||
            ])
 | 
			
		||||
 | 
			
		||||
        @endif
 | 
			
		||||
 | 
			
		||||
        <div id="password-input-container" @if(!isset($model)) style="display: none;" @endif>
 | 
			
		||||
            <p class="small">{{ trans('settings.users_password_desc') }}</p>
 | 
			
		||||
            @if(isset($model))
 | 
			
		||||
                <p class="small">
 | 
			
		||||
@@ -67,4 +82,6 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
@endif
 | 
			
		||||
@@ -217,6 +217,10 @@ Route::post('/register/confirm/resend', 'Auth\ConfirmEmailController@resend');
 | 
			
		||||
Route::get('/register/confirm/{token}', 'Auth\ConfirmEmailController@confirm');
 | 
			
		||||
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...
 | 
			
		||||
Route::get('/password/email', 'Auth\ForgotPasswordController@showLinkRequestForm');
 | 
			
		||||
Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user