diff --git a/.env.dev b/.env.dev index 1f86fab5e..7e967bdc7 100644 --- a/.env.dev +++ b/.env.dev @@ -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 "*". diff --git a/.env.example b/.env.example index 5853ae1ec..c8e75fb58 100644 --- a/.env.example +++ b/.env.example @@ -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. diff --git a/CHANGELOG b/CHANGELOG index e9fb4d401..3b64d5fbb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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: diff --git a/app/Http/Controllers/Auth/EmailChangeController.php b/app/Http/Controllers/Auth/EmailChangeController.php new file mode 100644 index 000000000..e35cf929d --- /dev/null +++ b/app/Http/Controllers/Auth/EmailChangeController.php @@ -0,0 +1,121 @@ +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)); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index b2e119962..5eac005d8 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -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 { diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 2b479c770..b4e8394b2 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -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')); + } + } } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index 637d3644d..5edb99229 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -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 { diff --git a/app/Http/Controllers/Auth/Validate2faController.php b/app/Http/Controllers/Auth/Validate2faController.php new file mode 100644 index 000000000..414edbcea --- /dev/null +++ b/app/Http/Controllers/Auth/Validate2faController.php @@ -0,0 +1,24 @@ +has('url')) { + return redirect($request->get('url')); + } + + return redirect('/'); + } +} diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 9123f6415..4d3efe9fe 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -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'])); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index fcd3e12a8..0f41e9f11 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -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, ]; } diff --git a/app/Http/Middleware/AuthEmailConfirm.php b/app/Http/Middleware/AuthEmailConfirm.php new file mode 100644 index 000000000..53374a567 --- /dev/null +++ b/app/Http/Middleware/AuthEmailConfirm.php @@ -0,0 +1,32 @@ +user()->confirmed) { + // Logout the user + Auth::guard()->logout(); + $request->session()->invalidate(); + + return redirect('/') + ->with('confirmation-danger', trans('confirmation::confirmation.again')); + } + } + + return $next($request); + } +} diff --git a/app/Http/Requests/EmailChangeRequest.php b/app/Http/Requests/EmailChangeRequest.php new file mode 100644 index 000000000..65a3548c8 --- /dev/null +++ b/app/Http/Requests/EmailChangeRequest.php @@ -0,0 +1,28 @@ + 'required|email|max:255|unique:users,email', + ]; + } +} diff --git a/app/Jobs/SendNewUserAlert.php b/app/Jobs/SendNewUserAlert.php index 38568f186..c50565820 100644 --- a/app/Jobs/SendNewUserAlert.php +++ b/app/Jobs/SendNewUserAlert.php @@ -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)); + } } } diff --git a/app/Notifications/ConfirmEmail.php b/app/Notifications/ConfirmEmail.php new file mode 100644 index 000000000..c3a445120 --- /dev/null +++ b/app/Notifications/ConfirmEmail.php @@ -0,0 +1,45 @@ +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")); + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 5222dd4e8..ad87511a7 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -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'); + }); + } } diff --git a/composer.json b/composer.json index c71d2ce3a..fff13a401 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 56d05667a..b7bfd00e9 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/app.php b/config/app.php index 6736be0d8..ea91c2a4f 100644 --- a/config/app.php +++ b/config/app.php @@ -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 diff --git a/config/monica.php b/config/monica.php index 307fd2929..06106d1a0 100644 --- a/config/monica.php +++ b/config/monica.php @@ -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 diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index fffaa51ac..710d29213 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -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, ]; }); diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php index 2f5b0d673..85a4c11cb 100644 --- a/resources/lang/en/auth.php +++ b/resources/lang/en/auth.php @@ -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 you’ve read and agree to our Privacy Policy and Terms of use.', 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve 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 click here.', + '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.', ]; diff --git a/resources/lang/en/mail.php b/resources/lang/en/mail.php index efb8520c3..345ca3a2a 100644 --- a/resources/lang/en/mail.php +++ b/resources/lang/en/mail.php @@ -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 you’re 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', ]; diff --git a/resources/lang/vendor/confirmation/en/confirmation.php b/resources/lang/vendor/confirmation/en/confirmation.php new file mode 100644 index 000000000..b6721dbc4 --- /dev/null +++ b/resources/lang/vendor/confirmation/en/confirmation.php @@ -0,0 +1,10 @@ + '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.' . + '
If you have not received the confirmation email check your spam folder.'. + '
To get a new confirmation email please click here.', + 'resend' => 'A confirmation message has been sent. Please check your mailbox.' +]; diff --git a/resources/views/auth/emailchange1.blade.php b/resources/views/auth/emailchange1.blade.php new file mode 100644 index 000000000..af90c87a2 --- /dev/null +++ b/resources/views/auth/emailchange1.blade.php @@ -0,0 +1,49 @@ +@extends('marketing.skeleton') + +@section('content') + +
+
+
+ + +
+
+
+ +@endsection diff --git a/resources/views/auth/emailchange2.blade.php b/resources/views/auth/emailchange2.blade.php new file mode 100644 index 000000000..3d780aca5 --- /dev/null +++ b/resources/views/auth/emailchange2.blade.php @@ -0,0 +1,50 @@ +@extends('marketing.skeleton') + +@section('content') + +
+
+
+ +
+ +
+
+ + + +@endsection diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index e66b96231..d7d2aad28 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -15,6 +15,24 @@

{{ trans('auth.login_to_account') }}

@include ('partials.errors') + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + @if (session('confirmation-success')) +
+ {{ session('confirmation-success') }} +
+ @endif + @if (session('confirmation-danger')) +
+ {!! session('confirmation-danger') !!} +
+
+ {!! trans('auth.confirmation_again', ['url' => url('settings/emailchange1')]) !!} +
+ @endif
{{ csrf_field() }} diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 4f2ab6460..0ba9ad022 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -21,6 +21,11 @@ @endif @include ('partials.errors') + @if (session('confirmation-success')) +
+ {{ session('confirmation-success') }} +
+ @endif {{ csrf_field() }} diff --git a/resources/views/auth/validate2fa.blade.php b/resources/views/auth/validate2fa.blade.php index 8c54e8816..445294be9 100644 --- a/resources/views/auth/validate2fa.blade.php +++ b/resources/views/auth/validate2fa.blade.php @@ -28,6 +28,7 @@ + {{-- TODO
diff --git a/resources/views/vendor/notifications/email.blade.php b/resources/views/vendor/notifications/email.blade.php index 66a35f53f..f1eec9648 100644 --- a/resources/views/vendor/notifications/email.blade.php +++ b/resources/views/vendor/notifications/email.blade.php @@ -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) $actionUrl, 'color' => $color]) {{ $actionText }} @endcomponent -@endif +@endisset {{-- Outro Lines --}} @foreach ($outroLines as $line) @@ -41,18 +41,22 @@ @endforeach - +{{-- Salutation --}} @if (! empty($salutation)) {{ $salutation }} @else -Regards,
{{ config('app.name') }} +@lang('mail.notifications_regards'),
{{ config('app.name') }} @endif - -@if (isset($actionText)) +{{-- Subcopy --}} +@isset($actionText) @component('mail::subcopy') -If you’re 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 diff --git a/routes/special.php b/routes/special.php new file mode 100644 index 000000000..3c74bc008 --- /dev/null +++ b/routes/special.php @@ -0,0 +1,11 @@ +group(function () { + Route::get('/settings/emailchange2', 'Auth\EmailChangeController@index'); + Route::post('/settings/emailchange2', 'Auth\EmailChangeController@save'); +}); diff --git a/routes/web.php b/routes/web.php index 835c8bb0d..17ada509a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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'); diff --git a/tests/Browser/Settings/MultiFAControllerTest.php b/tests/Browser/Settings/MultiFAControllerTest.php index e72b9c014..b52f74100 100644 --- a/tests/Browser/Settings/MultiFAControllerTest.php +++ b/tests/Browser/Settings/MultiFAControllerTest.php @@ -255,7 +255,7 @@ class MultiFAControllerTest extends DuskTestCase ->press('verify'); $this->assertFalse($this->hasDivAlert($browser)); - $browser->assertPathIs('/validate2fa'); + $browser->assertPathIs('/dashboard'); }); }