From 6376302ed961bcd4208a655a42479738c1655ce9 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Mon, 29 Feb 2016 16:46:27 -0500 Subject: [PATCH] Add option for client portal password --- app/Http/Controllers/AccountController.php | 10 +- .../Controllers/Auth/ClientAuthController.php | 127 ++++++++++++++++++ app/Http/routes.php | 24 ++-- app/Models/Client.php | 10 +- app/Models/Contact.php | 8 +- app/Providers/AuthServiceProvider.php | 46 +++++++ config/auth.php | 57 +------- .../2016_02_25_152948_add_client_password.php | 48 +++++++ resources/lang/en/texts.php | 6 + .../views/accounts/client_portal.blade.php | 60 ++++++--- resources/views/clients/edit.blade.php | 5 +- resources/views/invoices/edit.blade.php | 2 +- 12 files changed, 311 insertions(+), 92 deletions(-) create mode 100644 app/Http/Controllers/Auth/ClientAuthController.php create mode 100644 app/Providers/AuthServiceProvider.php create mode 100644 database/migrations/2016_02_25_152948_add_client_password.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 6c0f59a3ba..f499c779eb 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -150,7 +150,7 @@ class AccountController extends BaseController } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { return self::showInvoiceDesign($section); } elseif ($section == ACCOUNT_CLIENT_PORTAL) { - return self::showClientViewStyling(); + return self::showClientPortal(); } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { return self::showTemplates(); } elseif ($section === ACCOUNT_PRODUCTS) { @@ -398,7 +398,7 @@ class AccountController extends BaseController return View::make("accounts.{$section}", $data); } - private function showClientViewStyling() + private function showClientPortal() { $account = Auth::user()->account->load('country'); $css = $account->client_view_css ? $account->client_view_css : ''; @@ -414,6 +414,9 @@ class AccountController extends BaseController $data = [ 'client_view_css' => $css, + 'enable_portal_password' => $account->enable_portal_password, + 'fill_portal_password' => $account->fill_portal_password, + 'send_portal_password' => $account->send_portal_password, 'title' => trans("texts.client_portal"), 'section' => ACCOUNT_CLIENT_PORTAL, ]; @@ -528,6 +531,9 @@ class AccountController extends BaseController $account = Auth::user()->account; $account->client_view_css = $sanitized_css; + $account->enable_portal_password = Input::get('enable_portal_password'); + $account->fill_portal_password = Input::get('fill_portal_password'); + $account->send_portal_password = Input::get('send_portal_password'); $account->save(); Session::flash('message', trans('texts.updated_settings')); diff --git a/app/Http/Controllers/Auth/ClientAuthController.php b/app/Http/Controllers/Auth/ClientAuthController.php new file mode 100644 index 0000000000..263dd8e4f7 --- /dev/null +++ b/app/Http/Controllers/Auth/ClientAuthController.php @@ -0,0 +1,127 @@ +auth = $auth; + $this->registrar = $registrar; + $this->accountRepo = $repo; + $this->authService = $authService; + + //$this->middleware('guest', ['except' => 'getLogout']); + } + + public function authLogin($provider, Request $request) + { + return $this->authService->execute($provider, $request->has('code')); + } + + public function authUnlink() + { + $this->accountRepo->unlinkUserFromOauth(Auth::user()); + + Session::flash('message', trans('texts.updated_settings')); + return redirect()->to('/settings/' . ACCOUNT_USER_DETAILS); + } + + public function getLoginWrapper() + { + if (!Utils::isNinja() && !User::count()) { + return redirect()->to('invoice_now'); + } + + return self::getLogin(); + } + + public function postLoginWrapper(Request $request) + { + + $userId = Auth::check() ? Auth::user()->id : null; + $user = User::where('email', '=', $request->input('email'))->first(); + + if ($user && $user->failed_logins >= MAX_FAILED_LOGINS) { + Session::flash('error', trans('texts.invalid_credentials')); + return redirect()->to('login'); + } + + $response = self::postLogin($request); + + if (Auth::check()) { + Event::fire(new UserLoggedIn()); + + $users = false; + // we're linking a new account + if ($request->link_accounts && $userId && Auth::user()->id != $userId) { + $users = $this->accountRepo->associateAccounts($userId, Auth::user()->id); + Session::flash('message', trans('texts.associated_accounts')); + // check if other accounts are linked + } else { + $users = $this->accountRepo->loadAccounts(Auth::user()->id); + } + Session::put(SESSION_USER_ACCOUNTS, $users); + + } elseif ($user) { + $user->failed_logins = $user->failed_logins + 1; + $user->save(); + } + + return $response; + } + + + public function getLogoutWrapper() + { + if (Auth::check() && !Auth::user()->registered) { + $account = Auth::user()->account; + $this->accountRepo->unlinkAccount($account); + $account->forceDelete(); + } + + $response = self::getLogout(); + + Session::flush(); + + return $response; + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index f7bed59f68..9b5af76986 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -35,17 +35,19 @@ Route::get('/keep_alive', 'HomeController@keepAlive'); Route::post('/get_started', 'AccountController@getStarted'); // Client visible pages -Route::get('view/{invitation_key}', 'PublicClientController@view'); -Route::get('download/{invitation_key}', 'PublicClientController@download'); -Route::get('view', 'HomeController@viewLogo'); -Route::get('approve/{invitation_key}', 'QuoteController@approve'); -Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment'); -Route::post('payment/{invitation_key}', 'PaymentController@do_payment'); -Route::get('complete', 'PaymentController@offsite_payment'); -Route::get('client/quotes', 'PublicClientController@quoteIndex'); -Route::get('client/invoices', 'PublicClientController@invoiceIndex'); -Route::get('client/payments', 'PublicClientController@paymentIndex'); -Route::get('client/dashboard', 'PublicClientController@dashboard'); +Route::group(['middleware' => 'auth'], function() { + Route::get('view/{invitation_key}', 'PublicClientController@view'); + Route::get('download/{invitation_key}', 'PublicClientController@download'); + Route::get('view', 'HomeController@viewLogo'); + Route::get('approve/{invitation_key}', 'QuoteController@approve'); + Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment'); + Route::post('payment/{invitation_key}', 'PaymentController@do_payment'); + Route::get('complete', 'PaymentController@offsite_payment'); + Route::get('client/quotes', 'PublicClientController@quoteIndex'); + Route::get('client/invoices', 'PublicClientController@invoiceIndex'); + Route::get('client/payments', 'PublicClientController@paymentIndex'); + Route::get('client/dashboard', 'PublicClientController@dashboard'); +}); Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable')); Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable')); Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PublicClientController@paymentDatatable')); diff --git a/app/Models/Client.php b/app/Models/Client.php index 2167af0ddf..987ed45f3e 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -154,7 +154,15 @@ class Client extends EntityModel $contact = Contact::createNew(); $contact->send_invoice = true; } - + + if (!Utils::isPro() || $this->account->enable_portal_password){ + if(!empty($data['password']) && $data['password']!='-%unchanged%-'){ + $contact->password = bcrypt($data['password']); + } else if(empty($data['password'])){ + $contact->password = null; + } + } + $contact->fill($data); $contact->is_primary = $isPrimary; diff --git a/app/Models/Contact.php b/app/Models/Contact.php index a95f40bab0..9c86c4ce5b 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -3,10 +3,14 @@ use HTML; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Auth\Authenticatable; +use Illuminate\Auth\Passwords\CanResetPassword; +use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; +use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; -class Contact extends EntityModel +class Contact extends EntityModel implements AuthenticatableContract, CanResetPasswordContract { - use SoftDeletes; + use SoftDeletes, Authenticatable, CanResetPassword; protected $dates = ['deleted_at']; protected $fillable = [ diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000000..569762b389 --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,46 @@ +app->alias('customerauth', 'App\Auth\CustomerAuthManager'); + $this->app->alias('customerauth.driver', 'App\Auth\SiteGuard'); + $this->app->alias('customerauth.driver', 'App\Contracts\Auth\SiteGuard'); + + parent::register(); + } + + protected function registerAuthenticator() + { + $this->app->singleton('customerauth', function ($app) { + $app['customerauth.loaded'] = true; + + return new CustomerAuthManager($app); + }); + + $this->app->singleton('customerauth.driver', function ($app) { + return $app['customerauth']->driver(); + }); + } + + protected function registerUserResolver() + { + $this->app->bind('Illuminate\Contracts\Auth\Authenticatable', function ($app) { + return $app['customerauth']->user(); + }); + } + + protected function registerRequestRebindHandler() + { + $this->app->rebinding('request', function ($app, $request) { + $request->setUserResolver(function() use ($app) { + return $app['customerauth']->user(); + }); + }); + } +} \ No newline at end of file diff --git a/config/auth.php b/config/auth.php index 1ce67b55a1..12ebcf0337 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,67 +1,12 @@ 'eloquent', - - /* - |-------------------------------------------------------------------------- - | Authentication Model - |-------------------------------------------------------------------------- - | - | When using the "Eloquent" authentication driver, we need to know which - | Eloquent model should be used to retrieve your users. Of course, it - | is often just the "User" model but you may use whatever you like. - | - */ - 'model' => 'App\Models\User', - - /* - |-------------------------------------------------------------------------- - | Authentication Table - |-------------------------------------------------------------------------- - | - | When using the "Database" authentication driver, we need to know which - | table should be used to retrieve your users. We have chosen a basic - | default value but you may easily change it to any table you like. - | - */ - 'table' => 'users', - - /* - |-------------------------------------------------------------------------- - | Password Reset Settings - |-------------------------------------------------------------------------- - | - | Here you may set the options for resetting passwords including the view - | that is your password reset e-mail. You can also set the name of the - | table that maintains all of the reset tokens for your application. - | - | The expire time is the number of minutes that the reset token should be - | considered valid. This security feature keeps tokens short-lived so - | they have less time to be guessed. You may change this as needed. - | - */ - 'password' => [ 'email' => 'emails.password', 'table' => 'password_resets', 'expire' => 60, - ], - + ] ]; diff --git a/database/migrations/2016_02_25_152948_add_client_password.php b/database/migrations/2016_02_25_152948_add_client_password.php new file mode 100644 index 0000000000..f19f70b729 --- /dev/null +++ b/database/migrations/2016_02_25_152948_add_client_password.php @@ -0,0 +1,48 @@ +boolean('enable_portal_password')->default(0); + $table->boolean('fill_portal_password')->default(0); + $table->boolean('send_portal_password')->default(0); + }); + + Schema::table('contacts', function ($table) { + $table->string('password', 255)->nullable(); + $table->boolean('confirmation_code', 255)->nullable(); + $table->boolean('remember_token', 100)->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('accounts', function ($table) { + $table->dropColumn('enable_portal_password'); + $table->dropColumn('fill_portal_password'); + $table->dropColumn('send_portal_password'); + }); + + Schema::table('contacts', function ($table) { + $table->dropColumn('password'); + $table->dropColumn('confirmation_code'); + $table->dropColumn('remember_token'); + }); + } + +} diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 0c9bfac165..f888c6887e 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1028,6 +1028,12 @@ $LANG = array( 'user_unconfirmed' => 'Please confirm your account to send emails', 'invalid_contact_email' => 'Invalid contact email', ], + + // Client Passwords + 'client_portal_login_settings'=>'Login', + 'enable_portal_password'=>'Require a password', + 'send_portal_password'=>'Generate password automatically', + 'fill_portal_password'=>'Include password in invoice emails', ); return $LANG; diff --git a/resources/views/accounts/client_portal.blade.php b/resources/views/accounts/client_portal.blade.php index 5ba8fcf5bc..1e1eac04d3 100644 --- a/resources/views/accounts/client_portal.blade.php +++ b/resources/views/accounts/client_portal.blade.php @@ -13,6 +13,7 @@ ->addClass('warn-on-exit') !!} {!! Former::populateField('client_view_css', $client_view_css) !!} + {!! Former::populateField('enable_portal_password', $enable_portal_password) !!} @if (!Utils::isNinja() && !Auth::user()->account->isWhiteLabel())
@@ -26,23 +27,38 @@
- -
-
-

{!! trans('texts.custom_css') !!}

-
-
-
- {!! Former::textarea('client_view_css') - ->label(trans('texts.custom_css')) - ->rows(10) - ->raw() - ->autofocus() - ->maxlength(60000) - ->style("min-width:100%;max-width:100%;font-family:'Roboto Mono', 'Lucida Console', Monaco, monospace;font-size:14px;'") !!} -
-
-
+
+
+

{!! trans('texts.client_portal_login_settings') !!}

+
+
+ {!! Former::checkbox('enable_portal_password') + ->text(trans('texts.enable_portal_password')) + ->label(' ') !!} + {!! Former::checkbox('fill_portal_password') + ->text(trans('texts.fill_portal_password')) + ->label(' ') !!} + {!! Former::checkbox('send_portal_password') + ->text(trans('texts.send_portal_password')) + ->label(' ') !!} +
+
+
+
+

{!! trans('texts.custom_css') !!}

+
+
+
+ {!! Former::textarea('client_view_css') + ->label(trans('texts.custom_css')) + ->rows(10) + ->raw() + ->autofocus() + ->maxlength(60000) + ->style("min-width:100%;max-width:100%;font-family:'Roboto Mono', 'Lucida Console', Monaco, monospace;font-size:14px;'") !!} +
+
+
@@ -51,5 +67,13 @@ {!! Former::close() !!} - + @stop \ No newline at end of file diff --git a/resources/views/clients/edit.blade.php b/resources/views/clients/edit.blade.php index 72835f170f..4419ca45d0 100644 --- a/resources/views/clients/edit.blade.php +++ b/resources/views/clients/edit.blade.php @@ -93,7 +93,10 @@ attr: {name: 'contacts[' + \$index() + '][email]', id:'email'+\$index()}") !!} {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', attr: {name: 'contacts[' + \$index() + '][phone]'}") !!} - + @if (!Utils::isPro() || $account->enable_portal_password) + {!! Former::password('password')->data_bind("value: password()?'-%unchanged%-':'', valueUpdate: 'afterkeydown', + attr: {name: 'contacts[' + \$index() + '][password]'}") !!} + @endif
diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 8745c9d973..5405f9a582 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -529,7 +529,7 @@ ->addClass('client-email') !!} {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', attr: {name: 'client[contacts][' + \$index() + '][phone]'}") !!} - +f