Add email confirmation on signup (#1374)

This commit is contained in:
Alexis Saettler
2018-05-28 17:40:17 +02:00
committed by GitHub
parent 331daff1e1
commit a79c19dd6c
32 changed files with 574 additions and 25 deletions
+2
View File
@@ -38,6 +38,8 @@ APP_DEFAULT_LOCALE=en
# Ability to disable signups on your instance.
# Can be true or false. Default to false.
APP_DISABLE_SIGNUP=false
# Enable user email verification.
APP_SIGNUP_DOUBLE_OPTIN=false
# Set trusted proxy IP addresses. Useful for ssl terminating loadbalancers.
# To trust all proxies that connect directly to your server, use a "*".
+3
View File
@@ -60,6 +60,9 @@ APP_DEFAULT_LOCALE=en
# Can be true or false. Default to false.
APP_DISABLE_SIGNUP=true
# Enable user email verification.
APP_SIGNUP_DOUBLE_OPTIN=false
# Set trusted proxy IP addresses.
# To trust all proxies that connect directly to your server, use a "*".
# To trust one or more specific proxies that connect directly to your server, use a comma separated list of IP addresses.
+1
View File
@@ -10,6 +10,7 @@ UNRELEASED CHANGES:
* Fix duplication of tags when filtering contacts
* Add trusted proxies to run behind a ssl terminating loadbalancer
* Fix reminders for past events are visible on the dashboard
* Add email address verification on register, and email change
RELEASED VERSIONS:
@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\Auth;
use App\User;
use Illuminate\Http\Request;
use App\Notifications\ConfirmEmail;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\EmailChangeRequest;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class EmailChangeController extends Controller
{
use AuthenticatesUsers;
/**
* Where to redirect users after login / registration.
*
* @var string
*/
protected $redirectTo = '/settings/emailchange2';
/**
* Show the application's login form.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function showLoginFormSpecial(Request $request)
{
if ($request->session()->has('user_id')) {
$user = User::findOrFail($request->session()->get('user_id'));
return view('auth.emailchange1')
->with('email', $user->email);
}
return redirect('/');
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$user = auth()->user();
return view('auth.emailchange2')
->with('email', $user->email);
}
/**
* Change user email.
*
* @param EmailChangeRequest $request
* @return \Illuminate\Http\Response
*/
public function save(EmailChangeRequest $request)
{
$response = $this->validateAndEmailChange($request);
return $response == 'auth.email_changed'
? $this->sendChangedResponse($response)
: $this->sendChangedFailedResponse($response);
}
/**
* Validate a password change request and update password of the user.
*
* @param EmailChangeRequest $request
* @return mixed
*/
protected function validateAndEmailChange(EmailChangeRequest $request)
{
$user = auth()->user();
// Change email of the user
$user->email = $request->get('newmail');
// Resend validation token
$user->confirmation_code = str_random(30);
$user->confirmed = false;
$user->save();
$user->notify(new ConfirmEmail);
// Logout the user
Auth::guard()->logout();
$request->session()->invalidate();
return 'auth.email_changed';
}
/**
* Get the response for a successful password changed.
*
* @param string $response
* @return \Illuminate\Http\Response
*/
protected function sendChangedResponse($response)
{
return redirect('/')
->with('status', trans($response));
}
/**
* Get the response for a failed password.
*
* @param string $response
* @return \Illuminate\Http\Response
*/
protected function sendChangedFailedResponse($response)
{
return redirect('/')
->withErrors(trans($response));
}
}
@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Auth;
use App\Account;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Bestmomo\LaravelEmailConfirmation\Traits\AuthenticatesUsers;
class LoginController extends Controller
{
@@ -9,7 +9,7 @@ use App\Jobs\SendNewUserAlert;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
use Bestmomo\LaravelEmailConfirmation\Traits\RegistersUsers;
class RegisterController extends Controller
{
@@ -100,4 +100,26 @@ class RegisterController extends Controller
return $user;
}
/**
* The user has been registered.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function registered(Request $request, $user)
{
$first = Account::count() == 1;
if (! config('monica.signup_double_optin') || $first) {
// if signup_double_optin is disabled, skip the confirm email part
$user->confirmation_code = null;
$user->confirmed = true;
$user->save();
$this->guard()->login($user);
return redirect(route('login'));
}
}
}
@@ -3,7 +3,7 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Bestmomo\LaravelEmailConfirmation\Traits\ResetsPasswords;
class ResetPasswordController extends Controller
{
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class Validate2faController extends Controller
{
/**
* Redirect the user after 2fa form has been submitted.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if ($request->has('url')) {
return redirect($request->get('url'));
}
return redirect('/');
}
}
+15 -4
View File
@@ -12,6 +12,7 @@ use App\Jobs\ExportAccountAsSQL;
use App\Jobs\AddContactFromVCard;
use App\Jobs\SendInvitationEmail;
use Illuminate\Support\Facades\DB;
use App\Notifications\ConfirmEmail;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\ImportsRequest;
use App\Http\Requests\SettingsRequest;
@@ -72,11 +73,12 @@ class SettingsController extends Controller
*/
public function save(SettingsRequest $request)
{
$request->user()->update(
$user = $request->user();
$user->update(
$request->only([
'first_name',
'last_name',
'email',
'timezone',
'locale',
'currency_id',
@@ -86,8 +88,17 @@ class SettingsController extends Controller
]
);
$request->user()->account->default_time_reminder_is_sent = $request->get('reminder_time');
$request->user()->account->save();
if ($user->email != $request->get('email')) {
$user->email = $request->get('email');
$user->confirmation_code = str_random(30);
$user->confirmed = false;
$user->save();
$user->notify(new ConfirmEmail);
}
$user->account->default_time_reminder_is_sent = $request->get('reminder_time');
$user->account->save();
return redirect('settings')
->with('status', trans('settings.settings_success', [], $request['locale']));
+1
View File
@@ -61,5 +61,6 @@ class Kernel extends HttpKernel
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'2fa' => \PragmaRX\Google2FALaravel\Middleware::class,
'locale' => \App\Http\Middleware\CheckLocale::class,
'auth.confirm' => \App\Http\Middleware\AuthEmailConfirm::class,
];
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class AuthEmailConfirm
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (Auth::check()) {
if (! auth()->user()->confirmed) {
// Logout the user
Auth::guard()->logout();
$request->session()->invalidate();
return redirect('/')
->with('confirmation-danger', trans('confirmation::confirmation.again'));
}
}
return $next($request);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
class EmailChangeRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'newmail' => 'required|email|max:255|unique:users,email',
];
}
}
+5 -1
View File
@@ -33,6 +33,10 @@ class SendNewUserAlert implements ShouldQueue
*/
public function handle()
{
Mail::to(config('monica.email_new_user_notification'))->send(new NewUserAlert($this->user));
$email = config('monica.email_new_user_notification');
if (! empty($email)) {
Mail::to($email)
->send(new NewUserAlert($this->user));
}
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Notifications;
use App\Account;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class ConfirmEmail extends Notification implements ShouldQueue
{
use Queueable;
/**
* Get the notification's delivery channels.
*
* @return array
*/
public function via()
{
$first = Account::count() == 1;
if (! config('monica.signup_double_optin') || $first) {
return [];
}
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->subject(trans('mail.confirmation_email_title'))
->line(trans('mail.confirmation_email_title'))
->line(trans('mail.confirmation_email_intro'))
->action(trans('mail.confirmation_email_button'),
url("confirmation/$notifiable->id/$notifiable->confirmation_code"));
}
}
+19 -1
View File
@@ -173,7 +173,8 @@ class RouteServiceProvider extends ServiceProvider
$this->mapWebRoutes($router);
$this->mapOAuthRoutes($router);
//
$this->mapSpecialRoutes($router);
}
/**
@@ -227,4 +228,21 @@ class RouteServiceProvider extends ServiceProvider
require base_path('routes/api.php');
});
}
/**
* Define the "special" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapSpecialRoutes(Router $router)
{
$router->group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/special.php');
});
}
}
+1
View File
@@ -9,6 +9,7 @@
"ext-bcmath": "*",
"ext-intl": "*",
"bacon/bacon-qr-code": "^1.0",
"bestmomo/laravel-email-confirmation": "^1.1",
"creativeorange/gravatar": "~1.0",
"doctrine/dbal": "^2.5",
"erusev/parsedown": "~1.7",
Generated
+49 -3
View File
@@ -1,10 +1,10 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "654c35e2a59e4ad320a8eff5c8fc57c4",
"content-hash": "77646740ed930baa5e0db5adc6386704",
"packages": [
{
"name": "aws/aws-sdk-php",
@@ -176,6 +176,52 @@
],
"time": "2017-04-04T11:38:05+00:00"
},
{
"name": "bestmomo/laravel-email-confirmation",
"version": "V1.1.4",
"source": {
"type": "git",
"url": "https://github.com/bestmomo/laravel-email-confirmation.git",
"reference": "9fff1946e8a3e89231ae363f653a2178627054f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bestmomo/laravel-email-confirmation/zipball/9fff1946e8a3e89231ae363f653a2178627054f9",
"reference": "9fff1946e8a3e89231ae363f653a2178627054f9",
"shasum": ""
},
"require": {
"illuminate/support": "~5.4",
"php": ">=5.6.4"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Bestmomo\\LaravelEmailConfirmation\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Bestmomo\\LaravelEmailConfirmation\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bestmomo",
"email": "grandheretique@free.fr",
"homepage": "http://laravel.sillo.org"
}
],
"description": "Email address confirmation with Laravel",
"homepage": "http://github.com/laravel-email-confirmation",
"time": "2018-03-28T08:45:47+00:00"
},
{
"name": "colinodell/json5",
"version": "v1.0.4",
@@ -2066,7 +2112,7 @@
{
"name": "Luís Otávio Cobucci Oblonczyk",
"email": "lcobucci@gmail.com",
"role": "developer"
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
+13
View File
@@ -2,6 +2,19 @@
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application. This value is used when the
| framework needs to place the application's name in a notification or
| any other location as required by the application or its packages.
|
*/
'name' => env('APP_NAME', 'Monica'),
/*
|--------------------------------------------------------------------------
| Application Environment
+10
View File
@@ -12,6 +12,16 @@ return [
*/
'disable_signup' => env('APP_DISABLE_SIGNUP', false),
/*
|--------------------------------------------------------------------------
| Activate double optin on signup
|--------------------------------------------------------------------------
|
| Activates double optin on signup
|
*/
'signup_double_optin' => env('APP_SIGNUP_DOUBLE_OPTIN', true),
/*
|--------------------------------------------------------------------------
| New User Email Notification
+1
View File
@@ -20,6 +20,7 @@ $factory->define(App\User::class, function (Faker\Generator $faker) {
'remember_token' => str_random(10),
'timezone' => config('app.timezone'),
'name_order' => 'firstname_first',
'confirmed' => true,
'account_id' => factory(App\Account::class)->create()->id,
];
});
+6
View File
@@ -24,6 +24,7 @@ return [
'2fa_recuperation_code' => 'Enter a two factor recovery code',
'login_to_account' => 'Login to your account',
'login_again' => 'Please login again to your account',
'email' => 'Email',
'password' => 'Password',
'login' => 'Login',
@@ -59,4 +60,9 @@ return [
'register_policy' => 'Signing up signifies youve read and agree to our <a href=":url" hreflang=":hreflang">Privacy Policy</a> and <a href=":urlterm" hreflang=":hreflang">Terms of use</a>.',
'register_invitation_email' => 'For security purposes, please indicate the email of the person whove invited you to join this account. This information is provided in the invitation email.',
'confirmation_again' => 'If you want to change your email address you can <a href=":url" class="alert-link">click here</a>.',
'email_change_current_email' => 'Current email address:',
'email_change_title' => 'Change your email address',
'email_change_new' => 'New email address',
'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.',
];
+9
View File
@@ -13,4 +13,13 @@ return [
'stay_in_touch_subject_line' => 'Stay in touch with :name',
'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency days.',
'notifications_whoops' => 'Whoops!',
'notifications_hello' => 'Hello!',
'notifications_regards' => 'Regards',
'notifications_footer' => 'If youre having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)',
'confirmation_email_title' => 'Monica Email verification',
'confirmation_email_intro'=> 'To validate your email click on the button below',
'confirmation_email_button' => 'Verify email address',
];
+10
View File
@@ -0,0 +1,10 @@
<?php
return [
'message' => 'Thanks for signing up! Please check your emails to confirm your email address.',
'success' => 'You have successfully verified your account! You can now login.',
'again' => 'You must verify your email before you can access the site.' .
'<br/>If you have not received the confirmation email check your spam folder.'.
'<br/>To get a new confirmation email please <a href="' . url('confirmation/resend') . '" class="alert-link">click here</a>.',
'resend' => 'A confirmation message has been sent. Please check your mailbox.'
];
@@ -0,0 +1,49 @@
@extends('marketing.skeleton')
@section('content')
<body class="marketing register">
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-6 col-md-offset-3 col-md-offset-3-right">
<div class="signup-box">
<div class="dt w-100">
<div class="dtc tc">
<img class="" src="/img/monica.svg" width="97" height="88" alt="">
</div>
</div>
<h2>{{ trans('auth.email_change_title') }}</h2>
<h3>{{ trans('auth.login_again') }}</h3>
@include ('partials.errors')
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
<form class="" action="/settings/emailchange1" method="post">
{{ csrf_field() }}
<div class="form-group">
<label>{{ trans('auth.email_change_current_email') }}</label>
{{ $email }}
<input type="hidden" class="form-control" id="email" name="email" value="{{ $email }}">
</div>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="form-group actions">
<button type="submit" class="btn btn-primary">{{ trans('auth.login') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
@endsection
@@ -0,0 +1,50 @@
@extends('marketing.skeleton')
@section('content')
<body class="marketing register">
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-6 col-md-offset-3 col-md-offset-3-right">
<div class="signup-box">
<div class="dt w-100">
<div class="dtc tc">
<img class="" src="/img/monica.svg" width="97" height="88" alt="">
</div>
</div>
<h2>{{ trans('auth.email_change_title') }}</h2>
@include ('partials.errors')
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
<form action="/settings/emailchange2" method="POST">
{{ csrf_field() }}
<div class="form-group">
<label>{{ trans('auth.email_change_current_email') }}</label>
{{ $email }}
</div>
{{-- email address --}}
<div class="form-group">
<label for="newmail">{{ trans('auth.email_change_new') }}</label>
<input type="email" class="form-control" name="newmail" id="newmail" placeholder="{{ trans('settings.email_placeholder') }}" required>
<small id="emailHelp" class="form-text text-muted">{{ trans('settings.email_help') }}</small>
</div>
<button type="submit" class="btn btn-primary">{{ trans('app.save') }}</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
+18
View File
@@ -15,6 +15,24 @@
<h2>{{ trans('auth.login_to_account') }}</h2>
@include ('partials.errors')
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
@if (session('confirmation-success'))
<div class="alert alert-success">
{{ session('confirmation-success') }}
</div>
@endif
@if (session('confirmation-danger'))
<div class="alert alert-danger">
{!! session('confirmation-danger') !!}
</div>
<div class="alert alert-danger">
{!! trans('auth.confirmation_again', ['url' => url('settings/emailchange1')]) !!}
</div>
@endif
<form class="" action="/login" method="post">
{{ csrf_field() }}
+5
View File
@@ -21,6 +21,11 @@
@endif
@include ('partials.errors')
@if (session('confirmation-success'))
<div class="alert alert-success">
{{ session('confirmation-success') }}
</div>
@endif
<form class="" action="/register" method="post">
{{ csrf_field() }}
@@ -28,6 +28,7 @@
<label for="one_time_password">{{ trans('auth.2fa_one_time_password') }}</label>
<input type="number" class="form-control" id="one_time_password" name="one_time_password" required />
</div>
<input type=hidden name="url" value="{{ url()->current() }}" />
{{-- TODO
<div class="form-group checkbox">
+15 -11
View File
@@ -4,9 +4,9 @@
# {{ $greeting }}
@else
@if ($level == 'error')
# Whoops!
# @lang('mail.notifications_whoops')
@else
# Hello!
# @lang('mail.notifications_hello')
@endif
@endif
@@ -17,7 +17,7 @@
@endforeach
{{-- Action Button --}}
@if (isset($actionText))
@isset($actionText)
<?php
switch ($level) {
case 'success':
@@ -33,7 +33,7 @@
@component('mail::button', ['url' => $actionUrl, 'color' => $color])
{{ $actionText }}
@endcomponent
@endif
@endisset
{{-- Outro Lines --}}
@foreach ($outroLines as $line)
@@ -41,18 +41,22 @@
@endforeach
<!-- Salutation -->
{{-- Salutation --}}
@if (! empty($salutation))
{{ $salutation }}
@else
Regards,<br>{{ config('app.name') }}
@lang('mail.notifications_regards'),<br>{{ config('app.name') }}
@endif
<!-- Subcopy -->
@if (isset($actionText))
{{-- Subcopy --}}
@isset($actionText)
@component('mail::subcopy')
If youre having trouble clicking the "{{ $actionText }}" button, copy and paste the URL below
into your web browser: [{{ $actionUrl }}]({{ $actionUrl }})
@lang('mail.notifications_footer',
[
'actionText' => $actionText,
'actionURL' => $actionUrl
]
)
@endcomponent
@endif
@endisset
@endcomponent
+11
View File
@@ -0,0 +1,11 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('/settings/emailchange1', 'Auth\EmailChangeController@showLoginFormSpecial');
Route::post('/settings/emailchange1', 'Auth\EmailChangeController@login');
Route::middleware(['auth', '2fa'])->group(function () {
Route::get('/settings/emailchange2', 'Auth\EmailChangeController@index');
Route::post('/settings/emailchange2', 'Auth\EmailChangeController@save');
});
+4 -1
View File
@@ -34,13 +34,16 @@ Route::middleware(['auth'])->group(function () {
});
Route::middleware(['auth', '2fa'])->group(function () {
Route::post('/validate2fa', 'Auth\Validate2faController@index');
});
Route::middleware(['auth', 'auth.confirm', '2fa'])->group(function () {
Route::group(['as' => 'dashboard'], function () {
Route::get('/dashboard', 'DashboardController@index')->name('.index');
Route::get('/dashboard/calls', 'DashboardController@calls');
Route::get('/dashboard/notes', 'DashboardController@notes');
Route::post('/dashboard/setTab', 'DashboardController@setTab');
});
Route::post('/validate2fa', 'DashboardController@index');
Route::get('/compliance', 'ComplianceController@index')->name('compliance');
Route::post('/compliance/sign', 'ComplianceController@store');
@@ -255,7 +255,7 @@ class MultiFAControllerTest extends DuskTestCase
->press('verify');
$this->assertFalse($this->hasDivAlert($browser));
$browser->assertPathIs('/validate2fa');
$browser->assertPathIs('/dashboard');
});
}