1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-17 16:42:48 +01:00

Add option for client portal password

This commit is contained in:
Joshua Dwire 2016-02-29 16:46:27 -05:00
parent f26ab6fe5b
commit 6376302ed9
12 changed files with 311 additions and 92 deletions

View File

@ -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'));

View File

@ -0,0 +1,127 @@
<?php namespace App\Http\Controllers\Auth;
use Auth;
use Event;
use Utils;
use Session;
use Illuminate\Http\Request;
use App\Models\User;
use App\Events\UserLoggedIn;
use App\Http\Controllers\Controller;
use App\Ninja\Repositories\AccountRepository;
use App\Services\AuthService;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\Registrar;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
class ClientAuthController extends Controller {
/*
|--------------------------------------------------------------------------
| Registration & Login Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users, as well as the
| authentication of existing users. By default, this controller uses
| a simple trait to add these behaviors. Why don't you explore it?
|
*/
use AuthenticatesAndRegistersUsers;
protected $loginPath = '/client/login';
protected $redirectTo = '/client/dashboard';
protected $guard = 'contact';
protected $authService;
protected $accountRepo;
/**
* Create a new authentication controller instance.
*
* @param \Illuminate\Contracts\Auth\Guard $auth
* @param \Illuminate\Contracts\Auth\Registrar $registrar
* @return void
*/
public function __construct(Guard $auth, Registrar $registrar, AccountRepository $repo, AuthService $authService)
{
$this->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;
}
}

View File

@ -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'));

View File

@ -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;

View File

@ -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 = [

View File

@ -0,0 +1,46 @@
<?php namespace App\Auth;
use Illuminate\Auth\AuthServiceProvider;
use App\Auth\CustomerAuthManager;
use App\Auth\SiteGuard;
class CustomerAuthServiceProvider extends AuthServiceProvider
{
public function register()
{
$this->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();
});
});
}
}

View File

@ -1,67 +1,12 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Authentication Driver
|--------------------------------------------------------------------------
|
| This option controls the authentication driver that will be utilized.
| This driver manages the retrieval and authentication of the users
| attempting to get access to protected areas of your application.
|
| Supported: "database", "eloquent"
|
*/
'driver' => '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,
],
]
];

View File

@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddClientPassword extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function ($table) {
$table->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');
});
}
}

View File

@ -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;

View File

@ -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())
<div class="alert alert-warning" style="font-size:larger;">
@ -26,23 +27,38 @@
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.custom_css') !!}</h3>
</div>
<div class="panel-body">
<div class="col-md-10 col-md-offset-1">
{!! 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;'") !!}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.client_portal_login_settings') !!}</h3>
</div>
<div class="panel-body">
{!! Former::checkbox('enable_portal_password')
->text(trans('texts.enable_portal_password'))
->label('&nbsp;') !!}
{!! Former::checkbox('fill_portal_password')
->text(trans('texts.fill_portal_password'))
->label('&nbsp;') !!}
{!! Former::checkbox('send_portal_password')
->text(trans('texts.send_portal_password'))
->label('&nbsp;') !!}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.custom_css') !!}</h3>
</div>
<div class="panel-body">
<div class="col-md-10 col-md-offset-1">
{!! 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;'") !!}
</div>
</div>
</div>
</div>
</div>
@ -51,5 +67,13 @@
</center>
{!! Former::close() !!}
<script>
$('#enable_portal_password').change(fixCheckboxes);
function fixCheckboxes(){
var checked = $('#enable_portal_password').is(':checked');
$('#fill_portal_password').prop('disabled', !checked);
$('#send_portal_password').prop('disabled', !checked);
}
fixCheckboxes();
</script>
@stop

View File

@ -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
<div class="form-group">
<div class="col-lg-8 col-lg-offset-4 bold">
<span class="redlink bold" data-bind="visible: $parent.contacts().length > 1">

View File

@ -529,7 +529,7 @@
->addClass('client-email') !!}
{!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][phone]'}") !!}
f
<div class="form-group">
<div class="col-lg-8 col-lg-offset-4">
<span class="redlink bold" data-bind="visible: $parent.contacts().length > 1">