diff --git a/src/BackpackServiceProvider.php b/src/BackpackServiceProvider.php index dddcd142dd..66e0930f63 100644 --- a/src/BackpackServiceProvider.php +++ b/src/BackpackServiceProvider.php @@ -3,6 +3,7 @@ namespace Backpack\CRUD; use Backpack\Basset\Facades\Basset; +use Backpack\CRUD\app\Http\Middleware\EnsureEmailVerification; use Backpack\CRUD\app\Http\Middleware\ThrottlePasswordRecovery; use Backpack\CRUD\app\Library\CrudPanel\CrudPanel; use Backpack\CRUD\app\Library\Database\DatabaseSchema; @@ -129,6 +130,11 @@ public function registerMiddlewareGroup(Router $router) if (config('backpack.base.setup_password_recovery_routes')) { $router->aliasMiddleware('backpack.throttle.password.recovery', ThrottlePasswordRecovery::class); } + + // register the email verification middleware, if the developer enabled it in the config. + if (config('backpack.base.setup_email_verification_routes', false) && config('backpack.base.setup_email_verification_middleware', true)) { + $router->pushMiddlewareToGroup($middleware_key, EnsureEmailVerification::class); + } } public function publishFiles() diff --git a/src/app/Http/Controllers/Auth/RegisterController.php b/src/app/Http/Controllers/Auth/RegisterController.php index f25d95669d..e68e42e2cd 100644 --- a/src/app/Http/Controllers/Auth/RegisterController.php +++ b/src/app/Http/Controllers/Auth/RegisterController.php @@ -6,7 +6,8 @@ use Illuminate\Auth\Events\Registered; use Illuminate\Http\Request; use Illuminate\Routing\Controller; -use Validator; +use Illuminate\Support\Facades\Cookie; +use Illuminate\Support\Facades\Validator; class RegisterController extends Controller { @@ -64,7 +65,7 @@ protected function validator(array $data) * Create a new user instance after a valid registration. * * @param array $data - * @return User + * @return \Illuminate\Contracts\Auth\Authenticatable */ protected function create(array $data) { @@ -99,7 +100,7 @@ public function showRegistrationForm() * Handle a registration request for the application. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\Response|\Illuminate\Contracts\View\View */ public function register(Request $request) { @@ -113,6 +114,12 @@ public function register(Request $request) $user = $this->create($request->all()); event(new Registered($user)); + if (config('backpack.base.setup_email_verification_routes')) { + Cookie::queue('backpack_email_verification', $user->{config('backpack.base.email_column')}, 30); + + return redirect(route('verification.notice')); + } + $this->guard()->login($user); return redirect($this->redirectPath()); diff --git a/src/app/Http/Controllers/Auth/VerifyEmailController.php b/src/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000000..dba7d26ce9 --- /dev/null +++ b/src/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,88 @@ +getMiddleware()['signed'] ?? null) { + throw new Exception('Missing "signed" alias middleware in App/Http/Kernel.php. More info: https://backpackforlaravel.com/docs/6.x/base-how-to#enable-email-verification-in-backpack-routes'); + } + + $this->middleware('signed')->only('verifyEmail'); + $this->middleware('throttle:'.config('backpack.base.email_verification_throttle_access'))->only('resendVerificationEmail'); + + if (! backpack_users_have_email()) { + abort(500, trans('backpack::base.no_email_column')); + } + // where to redirect after the email is verified + $this->redirectTo = $this->redirectTo ?? backpack_url('dashboard'); + } + + public function emailVerificationRequired(Request $request): \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse + { + $this->getUserOrRedirect($request); + + return view(backpack_view('auth.verify-email')); + } + + /** + * Verify the user's email address. + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function verifyEmail(EmailVerificationRequest $request) + { + $this->getUserOrRedirect($request); + + $request->fulfill(); + + return redirect($this->redirectTo); + } + + /** + * Resend the email verification notification. + */ + public function resendVerificationEmail(Request $request): \Illuminate\Http\RedirectResponse + { + $user = $this->getUserOrRedirect($request); + + if (is_a($user, \Illuminate\Http\RedirectResponse::class)) { + return $user; + } + + $user->sendEmailVerificationNotification(); + Alert::success('Email verification link sent successfully.')->flash(); + + return back()->with('status', 'verification-link-sent'); + } + + private function getUser(Request $request): ?\Illuminate\Contracts\Auth\MustVerifyEmail + { + return $request->user(backpack_guard_name()) ?? (new UserFromCookie())(); + } + + private function getUserOrRedirect(Request $request): \Illuminate\Contracts\Auth\MustVerifyEmail|\Illuminate\Http\RedirectResponse + { + if ($user = $this->getUser($request)) { + return $user; + } + + return redirect()->route('backpack.auth.login'); + } +} diff --git a/src/app/Http/Middleware/EnsureEmailVerification.php b/src/app/Http/Middleware/EnsureEmailVerification.php new file mode 100644 index 0000000000..c641e9f2e2 --- /dev/null +++ b/src/app/Http/Middleware/EnsureEmailVerification.php @@ -0,0 +1,39 @@ +route()->getName(), ['verification.notice', 'verification.verify', 'verification.send'])) { + return $next($request); + } + + // the Laravel middleware needs the user resolver to be set with the backpack guard + $userResolver = $request->getUserResolver(); + $request->setUserResolver(function () use ($userResolver) { + return $userResolver(backpack_guard_name()); + }); + + try { + $verifiedMiddleware = new (app('router')->getMiddleware()['verified'])(); + } catch(Throwable) { + throw new Exception('Missing "verified" alias middleware in App/Http/Kernel.php. More info: https://backpackforlaravel.com/docs/6.x/base-how-to#enable-email-verification-in-backpack-routes'); + } + + return $verifiedMiddleware->handle($request, $next); + } +} diff --git a/src/app/Http/Requests/EmailVerificationRequest.php b/src/app/Http/Requests/EmailVerificationRequest.php new file mode 100644 index 0000000000..ae527505cc --- /dev/null +++ b/src/app/Http/Requests/EmailVerificationRequest.php @@ -0,0 +1,14 @@ +attemptLogin($request)) { + if (config('backpack.base.setup_email_verification_routes', false)) { + return $this->logoutIfEmailNotVerified($request); + } + return $this->sendLoginResponse($request); } @@ -199,4 +204,25 @@ protected function guard() { return Auth::guard(); } + + private function logoutIfEmailNotVerified(Request $request): Response|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + { + $user = $this->guard()->user(); + + // if the user is already verified, do nothing + if ($user->email_verified_at) { + return $this->sendLoginResponse($request); + } + // user is not yet verified, log him out + $this->guard()->logout(); + + // add a cookie for 30m to remember the email address that needs to be verified + Cookie::queue('backpack_email_verification', $user->{config('backpack.base.email_column')}, 30); + + if ($request->wantsJson()) { + return new Response('Email verification required', 403); + } + + return redirect(route('verification.notice')); + } } diff --git a/src/app/Library/Auth/UserFromCookie.php b/src/app/Library/Auth/UserFromCookie.php new file mode 100644 index 0000000000..56b76c0d06 --- /dev/null +++ b/src/app/Library/Auth/UserFromCookie.php @@ -0,0 +1,17 @@ +first(); + } + + return null; + } +} diff --git a/src/config/backpack/base.php b/src/config/backpack/base.php index 6275f97186..2dcd3a7410 100644 --- a/src/config/backpack/base.php +++ b/src/config/backpack/base.php @@ -56,6 +56,21 @@ // (you then need to manually define the routes in your web.php) 'setup_password_recovery_routes' => true, + // Set this to true if you would like to enable email verification for your user model. + // Make sure your user model implements the MustVerifyEmail contract and your database + // table contains the `email_verified_at` column. Read the following before enabling: + // https://backpackforlaravel.com/docs/6.x/base-how-to#enable-email-verification-in-backpack-routes + 'setup_email_verification_routes' => false, + + // When email verification is enabled, automatically add the Verified middleware to Backpack routes? + // Set false if you want to use your own Verified middleware in `middleware_class`. + 'setup_email_verification_middleware' => true, + + // How many times in any given time period should the user be allowed to + // request a new verification email? + // Defaults to 1,10 - 1 time in 10 minutes. + 'email_verification_throttle_access' => '3,15', + /* |-------------------------------------------------------------------------- | Security diff --git a/src/resources/lang/en/base.php b/src/resources/lang/en/base.php index 99a0ec9cbb..18e585dfba 100644 --- a/src/resources/lang/en/base.php +++ b/src/resources/lang/en/base.php @@ -85,4 +85,10 @@ 'throttled' => 'You have already requested a password reset recently. Please check your email. If you do not receive our email, please retry later.', 'throttled_request' => 'You have exceeded the limit of tries. Please wait a few minutes and try again.', + 'verify_email' => [ + 'email_verification' => 'Email Verification', + 'verification_link_sent' => 'A verification link has been sent to your email address.', + 'email_verification_required' => 'Please verify your email address, by clicking on the link we\'ve sent you.', + 'resend_verification_link' => 'Resend link', + ], ]; diff --git a/src/resources/views/ui/errors/layout.blade.php b/src/resources/views/ui/errors/layout.blade.php index 7ce60b3fca..39273768bb 100644 --- a/src/resources/views/ui/errors/layout.blade.php +++ b/src/resources/views/ui/errors/layout.blade.php @@ -1,5 +1,5 @@ +{{-- show error using sidebar layout if logged in AND on an admin page; otherwise use a blank page --}} @extends(backpack_view(backpack_user() && backpack_theme_config('layout') ? 'layouts.'.backpack_theme_config('layout') : 'errors.blank')) -{{-- show error using sidebar layout if looged in AND on an admin page; otherwise use a blank page --}} @section('content')