diff --git a/app/EmailConfirmation.php b/app/EmailConfirmation.php new file mode 100644 index 000000000..a8f8f80b8 --- /dev/null +++ b/app/EmailConfirmation.php @@ -0,0 +1,15 @@ +belongsTo('Oxbow\User'); + } +} diff --git a/app/Exceptions/ConfirmationEmailException.php b/app/Exceptions/ConfirmationEmailException.php new file mode 100644 index 000000000..570eed2e9 --- /dev/null +++ b/app/Exceptions/ConfirmationEmailException.php @@ -0,0 +1,7 @@ +middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister']]); $this->socialAuthService = $socialAuthService; + $this->emailConfirmationService = $emailConfirmationService; + $this->userRepo = $userRepo; + parent::__construct(); } /** @@ -52,7 +64,7 @@ class AuthController extends Controller return Validator::make($data, [ 'name' => 'required|max:255', 'email' => 'required|email|max:255|unique:users', - 'password' => 'required|confirmed|min:6', + 'password' => 'required|min:6', ]); } @@ -71,6 +83,13 @@ class AuthController extends Controller ]); } + protected function checkRegistrationAllowed() + { + if(!\Setting::get('registration-enabled')) { + throw new UserRegistrationException('Registrations are currently disabled.', '/login'); + } + } + /** * Show the application registration form. * @@ -78,10 +97,104 @@ class AuthController extends Controller */ public function getRegister() { + $this->checkRegistrationAllowed(); $socialDrivers = $this->socialAuthService->getActiveDrivers(); return view('auth.register', ['socialDrivers' => $socialDrivers]); } + /** + * Handle a registration request for the application. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + * @throws UserRegistrationException + */ + public function postRegister(Request $request) + { + $this->checkRegistrationAllowed(); + $validator = $this->validator($request->all()); + + if ($validator->fails()) { + $this->throwValidationException( + $request, $validator + ); + } + + if(\Setting::get('registration-restrict')) { + $restrictedEmailDomains = explode(',', str_replace(' ', '', \Setting::get('registration-restrict'))); + $userEmailDomain = $domain = substr(strrchr($request->get('email'), "@"), 1); + if(!in_array($userEmailDomain, $restrictedEmailDomains)) { + throw new UserRegistrationException('That email domain does not have access to this application', '/register'); + } + } + + $newUser = $this->create($request->all()); + $newUser->attachRoleId(\Setting::get('registration-role'), 1); + + if(\Setting::get('registration-confirmation') || \Setting::get('registration-restrict')) { + $newUser->email_confirmed = false; + $newUser->save(); + $this->emailConfirmationService->sendConfirmation($newUser); + return redirect('/register/confirm'); + } + + auth()->login($newUser); + return redirect($this->redirectPath()); + } + + /** + * Show the page to tell the user to check thier email + * and confirm their address. + */ + public function getRegisterConfirmation() + { + return view('auth/register-confirm'); + } + + /** + * Confirms an email via a token and logs the user into the system. + * @param $token + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws UserRegistrationException + */ + public function confirmEmail($token) + { + $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token); + $user = $confirmation->user; + $user->email_confirmed = true; + $user->save(); + auth()->login($confirmation->user); + session()->flash('success', 'Your email has been confirmed!'); + $this->emailConfirmationService->deleteConfirmationsByUser($user); + return redirect($this->redirectPath); + } + + /** + * Shows a notice that a user's email address has not been confirmed, + * Also has the option to re-send the confirmation email. + * @return \Illuminate\View\View + */ + public function showAwaitingConfirmation() + { + return view('auth/user-unconfirmed'); + } + + /** + * Resend the confirmation email + * @param Request $request + * @return \Illuminate\View\View + */ + public function resendConfirmation(Request $request) + { + $this->validate($request, [ + 'email' => 'required|email|exists:users,email' + ]); + $user = $this->userRepo->getByEmail($request->get('email')); + $this->emailConfirmationService->sendConfirmation($user); + \Session::flash('success', 'Confirmation email resent, Please check your inbox.'); + return redirect('/register/confirm'); + } + /** * Show the application login form. * diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index e0695f22d..5193a9d3b 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -38,6 +38,7 @@ class SettingController extends Controller $key = str_replace('setting-', '', trim($name)); Setting::put($key, $value); } + session()->flash('success', 'Settings Saved'); return redirect('/settings'); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index f6a8d13e3..9923e9d9c 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -118,7 +118,6 @@ class UserController extends Controller } if ($request->has('password') && $request->get('password') != '') { - //dd('cat'); $password = $request->get('password'); $user->password = bcrypt($password); } diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index f0b2f7eda..f3c6f5925 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -4,6 +4,7 @@ namespace Oxbow\Http\Middleware; use Closure; use Illuminate\Contracts\Auth\Guard; +use Oxbow\Exceptions\UserRegistrationException; use Setting; class Authenticate @@ -34,6 +35,9 @@ class Authenticate */ public function handle($request, Closure $next) { + if(auth()->check() && auth()->user()->email_confirmed == false) { + return redirect()->guest('/register/confirm/awaiting'); + } if ($this->auth->guest() && !Setting::get('app-public')) { if ($request->ajax()) { return response('Unauthorized.', 401); diff --git a/app/Http/routes.php b/app/Http/routes.php index be7ac8736..df2860ea5 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -88,6 +88,11 @@ Route::get('/login', 'Auth\AuthController@getLogin'); Route::post('/login', 'Auth\AuthController@postLogin'); Route::get('/logout', 'Auth\AuthController@getLogout'); Route::get('/register', 'Auth\AuthController@getRegister'); +Route::get('/register/confirm', 'Auth\AuthController@getRegisterConfirmation'); +Route::get('/register/confirm/awaiting', 'Auth\AuthController@showAwaitingConfirmation'); +Route::post('/register/confirm/resend', 'Auth\AuthController@resendConfirmation'); +Route::get('/register/confirm/{token}', 'Auth\AuthController@confirmEmail'); +Route::post('/register', 'Auth\AuthController@postRegister'); // Password reset link request routes... Route::get('/password/email', 'Auth\PasswordController@getEmail'); diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index f6c80ff42..43b3cf4ca 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -21,4 +21,9 @@ class UserRepo public function getByEmail($email) { return $this->user->where('email', '=', $email)->first(); } + + public function getById($id) + { + return $this->user->findOrFail($id); + } } \ No newline at end of file diff --git a/app/Services/EmailConfirmationService.php b/app/Services/EmailConfirmationService.php new file mode 100644 index 000000000..dd85ad834 --- /dev/null +++ b/app/Services/EmailConfirmationService.php @@ -0,0 +1,102 @@ +mailer = $mailer; + $this->emailConfirmation = $emailConfirmation; + } + + /** + * Create new confirmation for a user, + * Also removes any existing old ones. + * @param User $user + * @throws ConfirmationEmailException + */ + public function sendConfirmation(User $user) + { + if($user->email_confirmed) { + throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login'); + } + $this->deleteConfirmationsByUser($user); + $token = $this->getToken(); + $this->emailConfirmation->create([ + 'user_id' => $user->id, + 'token' => $token, + ]); + $this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) { + $appName = \Setting::get('app-name', 'BookStack'); + $message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.'); + }); + } + + /** + * Gets an email confirmation by looking up the token, + * Ensures the token has not expired. + * @param string $token + * @return EmailConfirmation + * @throws UserRegistrationException + */ + public function getEmailConfirmationFromToken($token) + { + $emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first(); + // If not found + if ($emailConfirmation === null) { + throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register'); + } + + // If more than a day old + if(Carbon::now()->subDay()->gt($emailConfirmation->created_at)) { + $this->sendConfirmation($emailConfirmation->user); + throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm'); + } + + return $emailConfirmation; + } + + + /** + * Delete all email confirmations that belong to a user. + * @param User $user + * @return mixed + */ + public function deleteConfirmationsByUser(User $user) + { + return $this->emailConfirmation->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->emailConfirmation->where('token', '=', $token)->exists()) { + $token = str_random(25); + } + return $token; + } + + +} \ No newline at end of file diff --git a/database/migrations/2015_09_04_165821_create_social_accounts_table.php b/database/migrations/2015_09_04_165821_create_social_accounts_table.php index a4f627390..3933e728f 100644 --- a/database/migrations/2015_09_04_165821_create_social_accounts_table.php +++ b/database/migrations/2015_09_04_165821_create_social_accounts_table.php @@ -14,8 +14,8 @@ class CreateSocialAccountsTable extends Migration { Schema::create('social_accounts', function (Blueprint $table) { $table->increments('id'); - $table->integer('user_id')->indexed(); - $table->string('driver')->indexed(); + $table->integer('user_id')->index(); + $table->string('driver')->index(); $table->string('driver_id'); $table->string('avatar'); $table->timestamps(); diff --git a/database/migrations/2015_09_05_164707_add_email_confirmation_table.php b/database/migrations/2015_09_05_164707_add_email_confirmation_table.php new file mode 100644 index 000000000..2f20cf0cf --- /dev/null +++ b/database/migrations/2015_09_05_164707_add_email_confirmation_table.php @@ -0,0 +1,39 @@ +boolean('email_confirmed')->default(true); + }); + + Schema::create('email_confirmations', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id')->index(); + $table->string('token')->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('email_confirmed'); + }); + Schema::drop('email_confirmations'); + } +} diff --git a/resources/views/auth/register-confirm.blade.php b/resources/views/auth/register-confirm.blade.php new file mode 100644 index 000000000..2f70b9ffd --- /dev/null +++ b/resources/views/auth/register-confirm.blade.php @@ -0,0 +1,19 @@ +@extends('public') + +@section('header-buttons') + @if(!$signedIn) + Sign in + @endif +@stop + +@section('content') + +
+
+

Thanks for registering!

+

Please check your email and click the confirmation button to access {{ \Setting::get('app-name') }}.

+
+
+ + +@stop diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 48f8ebaa8..f9f5d0149 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -8,9 +8,9 @@
-

Register

+

Sign Up

-
+ {!! csrf_field() !!}
@@ -29,13 +29,14 @@
- +
@if(count($socialDrivers) > 0)

Social Registration

+

Register and sign in using another service.

@if(isset($socialDrivers['google'])) @endif diff --git a/resources/views/auth/user-unconfirmed.blade.php b/resources/views/auth/user-unconfirmed.blade.php new file mode 100644 index 000000000..1b0a20a69 --- /dev/null +++ b/resources/views/auth/user-unconfirmed.blade.php @@ -0,0 +1,30 @@ +@extends('public') + +@section('content') + +
+
+

Email Address not confirmed

+

Your email address has not yet been confirmed.
+ Please click the link in the email that was sent shortly after you registered.
+ If you cannot find the email you can re-send the confirmation email by submitting the form below. +

+
+
+ {!! csrf_field() !!} +
+ + @if(auth()->check()) + @include('form/text', ['name' => 'email', 'model' => auth()->user()]) + @else + @include('form/text', ['name' => 'email']) + @endif +
+
+ +
+
+
+
+ +@stop diff --git a/resources/views/emails/email-confirmation.blade.php b/resources/views/emails/email-confirmation.blade.php index f2c9710a1..b2bba96ca 100644 --- a/resources/views/emails/email-confirmation.blade.php +++ b/resources/views/emails/email-confirmation.blade.php @@ -157,7 +157,7 @@
-

Confirm Email

+

Confirm Email

diff --git a/resources/views/public.blade.php b/resources/views/public.blade.php index eaff2c2d8..f252ce38c 100644 --- a/resources/views/public.blade.php +++ b/resources/views/public.blade.php @@ -37,6 +37,19 @@ + @if($signedIn) + {{ $currentUser->name }} + + @endif