mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-17 00:39:06 +08:00
Start user invite system
This commit is contained in:
parent
4de432b50d
commit
44330bdd24
@ -1,33 +1,18 @@
|
|||||||
<?php namespace BookStack\Auth\Access;
|
<?php namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\ConfirmationEmailException;
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
|
||||||
use BookStack\Notifications\ConfirmEmail;
|
use BookStack\Notifications\ConfirmEmail;
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\Connection as Database;
|
|
||||||
|
|
||||||
class EmailConfirmationService
|
class EmailConfirmationService extends UserTokenService
|
||||||
{
|
{
|
||||||
protected $db;
|
protected $tokenTable = 'email_confirmations';
|
||||||
protected $users;
|
protected $expiryTime = 24;
|
||||||
|
|
||||||
/**
|
|
||||||
* EmailConfirmationService constructor.
|
|
||||||
* @param Database $db
|
|
||||||
* @param \BookStack\Auth\UserRepo $users
|
|
||||||
*/
|
|
||||||
public function __construct(Database $db, UserRepo $users)
|
|
||||||
{
|
|
||||||
$this->db = $db;
|
|
||||||
$this->users = $users;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new confirmation for a user,
|
* Create new confirmation for a user,
|
||||||
* Also removes any existing old ones.
|
* Also removes any existing old ones.
|
||||||
* @param \BookStack\Auth\User $user
|
* @param User $user
|
||||||
* @throws ConfirmationEmailException
|
* @throws ConfirmationEmailException
|
||||||
*/
|
*/
|
||||||
public function sendConfirmation(User $user)
|
public function sendConfirmation(User $user)
|
||||||
@ -36,76 +21,10 @@ class EmailConfirmationService
|
|||||||
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
|
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->deleteConfirmationsByUser($user);
|
$this->deleteByUser($user);
|
||||||
$token = $this->createEmailConfirmation($user);
|
$token = $this->createTokenForUser($user);
|
||||||
|
|
||||||
$user->notify(new ConfirmEmail($token));
|
$user->notify(new ConfirmEmail($token));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new email confirmation in the database and returns the token.
|
|
||||||
* @param User $user
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function createEmailConfirmation(User $user)
|
|
||||||
{
|
|
||||||
$token = $this->getToken();
|
|
||||||
$this->db->table('email_confirmations')->insert([
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'token' => $token,
|
|
||||||
'created_at' => Carbon::now(),
|
|
||||||
'updated_at' => Carbon::now()
|
|
||||||
]);
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an email confirmation by looking up the token,
|
|
||||||
* Ensures the token has not expired.
|
|
||||||
* @param string $token
|
|
||||||
* @return array|null|\stdClass
|
|
||||||
* @throws UserRegistrationException
|
|
||||||
*/
|
|
||||||
public function getEmailConfirmationFromToken($token)
|
|
||||||
{
|
|
||||||
$emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
|
|
||||||
|
|
||||||
// If not found show error
|
|
||||||
if ($emailConfirmation === null) {
|
|
||||||
throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If more than a day old
|
|
||||||
if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
|
|
||||||
$user = $this->users->getById($emailConfirmation->user_id);
|
|
||||||
$this->sendConfirmation($user);
|
|
||||||
throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
|
|
||||||
}
|
|
||||||
|
|
||||||
$emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
|
|
||||||
return $emailConfirmation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete all email confirmations that belong to a user.
|
|
||||||
* @param \BookStack\Auth\User $user
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function deleteConfirmationsByUser(User $user)
|
|
||||||
{
|
|
||||||
return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a unique token within the email confirmation database.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function getToken()
|
|
||||||
{
|
|
||||||
$token = str_random(24);
|
|
||||||
while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
|
|
||||||
$token = str_random(25);
|
|
||||||
}
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
23
app/Auth/Access/UserInviteService.php
Normal file
23
app/Auth/Access/UserInviteService.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Notifications\UserInvite;
|
||||||
|
|
||||||
|
class UserInviteService extends UserTokenService
|
||||||
|
{
|
||||||
|
protected $tokenTable = 'user_invites';
|
||||||
|
protected $expiryTime = 336; // Two weeks
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an invitation to a user to sign into BookStack
|
||||||
|
* Removes existing invitation tokens.
|
||||||
|
* @param User $user
|
||||||
|
*/
|
||||||
|
public function sendInvitation(User $user)
|
||||||
|
{
|
||||||
|
$this->deleteByUser($user);
|
||||||
|
$token = $this->createTokenForUser($user);
|
||||||
|
$user->notify(new UserInvite($token));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
134
app/Auth/Access/UserTokenService.php
Normal file
134
app/Auth/Access/UserTokenService.php
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<?php namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Exceptions\UserTokenExpiredException;
|
||||||
|
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Connection as Database;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class UserTokenService
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of table where user tokens are stored.
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $tokenTable = 'user_tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token expiry time in hours.
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $expiryTime = 24;
|
||||||
|
|
||||||
|
protected $db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserTokenService constructor.
|
||||||
|
* @param Database $db
|
||||||
|
*/
|
||||||
|
public function __construct(Database $db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all email confirmations that belong to a user.
|
||||||
|
* @param User $user
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function deleteByUser(User $user)
|
||||||
|
{
|
||||||
|
return $this->db->table($this->tokenTable)
|
||||||
|
->where('user_id', '=', $user->id)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user id from a token, while check the token exists and has not expired.
|
||||||
|
* @param string $token
|
||||||
|
* @return int
|
||||||
|
* @throws UserTokenNotFoundException
|
||||||
|
* @throws UserTokenExpiredException
|
||||||
|
*/
|
||||||
|
public function checkTokenAndGetUserId(string $token) : int
|
||||||
|
{
|
||||||
|
$entry = $this->getEntryByToken($token);
|
||||||
|
|
||||||
|
if (is_null($entry)) {
|
||||||
|
throw new UserTokenNotFoundException('Token "' . $token . '" not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->entryExpired($entry)) {
|
||||||
|
throw new UserTokenExpiredException("Token of id {$token->id} has expired.", $entry->user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entry->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a unique token within the email confirmation database.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function generateToken() : string
|
||||||
|
{
|
||||||
|
$token = str_random(24);
|
||||||
|
while ($this->tokenExists($token)) {
|
||||||
|
$token = str_random(25);
|
||||||
|
}
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate and store a token for the given user.
|
||||||
|
* @param User $user
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function createTokenForUser(User $user) : string
|
||||||
|
{
|
||||||
|
$token = $this->generateToken();
|
||||||
|
$this->db->table($this->tokenTable)->insert([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'token' => $token,
|
||||||
|
'created_at' => Carbon::now(),
|
||||||
|
'updated_at' => Carbon::now()
|
||||||
|
]);
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given token exists.
|
||||||
|
* @param string $token
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function tokenExists(string $token) : bool
|
||||||
|
{
|
||||||
|
return $this->db->table($this->tokenTable)
|
||||||
|
->where('token', '=', $token)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a token entry for the given token.
|
||||||
|
* @param string $token
|
||||||
|
* @return object|null
|
||||||
|
*/
|
||||||
|
protected function getEntryByToken(string $token)
|
||||||
|
{
|
||||||
|
return $this->db->table($this->tokenTable)
|
||||||
|
->where('token', '=', $token)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given token entry has expired.
|
||||||
|
* @param stdClass $tokenEntry
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function entryExpired(stdClass $tokenEntry) : bool
|
||||||
|
{
|
||||||
|
return Carbon::now()->subHours($this->expiryTime)
|
||||||
|
->gt(new Carbon($tokenEntry->created_at));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
use BookStack\Notifications\ResetPassword;
|
use BookStack\Notifications\ResetPassword;
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Auth\Authenticatable;
|
use Illuminate\Auth\Authenticatable;
|
||||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||||
@ -10,6 +11,20 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class User
|
||||||
|
* @package BookStack\Auth
|
||||||
|
* @property string $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $email
|
||||||
|
* @property string $password
|
||||||
|
* @property Carbon $created_at
|
||||||
|
* @property Carbon $updated_at
|
||||||
|
* @property bool $email_confirmed
|
||||||
|
* @property int $image_id
|
||||||
|
* @property string $external_auth_id
|
||||||
|
* @property string $system_name
|
||||||
|
*/
|
||||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
||||||
{
|
{
|
||||||
use Authenticatable, CanResetPassword, Notifiable;
|
use Authenticatable, CanResetPassword, Notifiable;
|
||||||
|
19
app/Exceptions/UserTokenExpiredException.php
Normal file
19
app/Exceptions/UserTokenExpiredException.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
class UserTokenExpiredException extends \Exception {
|
||||||
|
|
||||||
|
public $userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserTokenExpiredException constructor.
|
||||||
|
* @param string $message
|
||||||
|
* @param int $userId
|
||||||
|
*/
|
||||||
|
public function __construct(string $message, int $userId)
|
||||||
|
{
|
||||||
|
$this->userId = $userId;
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
3
app/Exceptions/UserTokenNotFoundException.php
Normal file
3
app/Exceptions/UserTokenNotFoundException.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
class UserTokenNotFoundException extends \Exception {}
|
@ -7,10 +7,13 @@ use BookStack\Auth\Access\SocialAuthService;
|
|||||||
use BookStack\Auth\SocialAccount;
|
use BookStack\Auth\SocialAccount;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||||
use BookStack\Exceptions\SocialSignInException;
|
use BookStack\Exceptions\SocialSignInException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
|
use BookStack\Exceptions\UserTokenExpiredException;
|
||||||
|
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||||
@ -189,17 +192,38 @@ class RegisterController extends Controller
|
|||||||
* Confirms an email via a token and logs the user into the system.
|
* Confirms an email via a token and logs the user into the system.
|
||||||
* @param $token
|
* @param $token
|
||||||
* @return RedirectResponse|Redirector
|
* @return RedirectResponse|Redirector
|
||||||
* @throws UserRegistrationException
|
* @throws ConfirmationEmailException
|
||||||
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function confirmEmail($token)
|
public function confirmEmail($token)
|
||||||
{
|
{
|
||||||
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
|
try {
|
||||||
$user = $confirmation->user;
|
$userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
|
||||||
|
if ($exception instanceof UserTokenNotFoundException) {
|
||||||
|
session()->flash('error', trans('errors.email_confirmation_invalid'));
|
||||||
|
return redirect('/register');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exception instanceof UserTokenExpiredException) {
|
||||||
|
$user = $this->userRepo->getById($exception->userId);
|
||||||
|
$this->emailConfirmationService->sendConfirmation($user);
|
||||||
|
session()->flash('error', trans('errors.email_confirmation_expired'));
|
||||||
|
return redirect('/register/confirm');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->userRepo->getById($userId);
|
||||||
$user->email_confirmed = true;
|
$user->email_confirmed = true;
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
auth()->login($user);
|
auth()->login($user);
|
||||||
session()->flash('success', trans('auth.email_confirm_success'));
|
session()->flash('success', trans('auth.email_confirm_success'));
|
||||||
$this->emailConfirmationService->deleteConfirmationsByUser($user);
|
$this->emailConfirmationService->deleteByUser($user);
|
||||||
|
|
||||||
return redirect($this->redirectPath);
|
return redirect($this->redirectPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
app/Notifications/UserInvite.php
Normal file
31
app/Notifications/UserInvite.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php namespace BookStack\Notifications;
|
||||||
|
|
||||||
|
class UserInvite extends MailNotification
|
||||||
|
{
|
||||||
|
public $token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
* @param string $token
|
||||||
|
*/
|
||||||
|
public function __construct($token)
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||||
|
*/
|
||||||
|
public function toMail($notifiable)
|
||||||
|
{
|
||||||
|
$appName = ['appName' => setting('app-name')];
|
||||||
|
return $this->newMailMessage()
|
||||||
|
->subject(trans('auth.user_invite_email_subject', $appName))
|
||||||
|
->greeting(trans('auth.user_invite_email_greeting', $appName))
|
||||||
|
->line(trans('auth.user_invite_email_text'))
|
||||||
|
->action(trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddUserInvitesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('user_invites', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->integer('user_id')->index();
|
||||||
|
$table->string('token')->index();
|
||||||
|
$table->nullableTimestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_invites');
|
||||||
|
}
|
||||||
|
}
|
@ -64,4 +64,10 @@ return [
|
|||||||
'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',
|
'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',
|
||||||
'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',
|
'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',
|
||||||
'email_not_confirmed_resend_button' => 'Resend Confirmation Email',
|
'email_not_confirmed_resend_button' => 'Resend Confirmation Email',
|
||||||
|
|
||||||
|
// 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_text' => 'Click the button below to set an account password and gain access:',
|
||||||
|
'user_invite_email_action' => 'Set Account Password',
|
||||||
];
|
];
|
@ -27,6 +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.',
|
||||||
|
|
||||||
// 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.',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user