diff --git a/app/Http/Controllers/AccountApiController.php b/app/Http/Controllers/AccountApiController.php index 0de2498139..f36252bc8d 100644 --- a/app/Http/Controllers/AccountApiController.php +++ b/app/Http/Controllers/AccountApiController.php @@ -19,6 +19,8 @@ use App\Ninja\Transformers\UserAccountTransformer; use App\Http\Controllers\BaseAPIController; use Swagger\Annotations as SWG; +use App\Events\UserSignedUp; +use App\Http\Requests\RegisterRequest; use App\Http\Requests\UpdateAccountRequest; class AccountApiController extends BaseAPIController @@ -32,13 +34,19 @@ class AccountApiController extends BaseAPIController $this->accountRepo = $accountRepo; } + public function register(RegisterRequest $request) + { + $account = $this->accountRepo->create($request->first_name, $request->last_name, $request->email, $request->password); + $user = $account->users()->first(); + + Auth::login($user, true); + event(new UserSignedUp()); + + return $this->processLogin($request); + } + public function login(Request $request) { - if ( ! env(API_SECRET) || $request->api_secret !== env(API_SECRET)) { - sleep(ERROR_DELAY); - return $this->errorResponse(['message'=>'Invalid secret'],401); - } - if (Auth::attempt(['email' => $request->email, 'password' => $request->password])) { return $this->processLogin($request); } else { diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 8d4af20011..a0fb51b746 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -26,7 +26,7 @@ use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\ReferralRepository; use App\Ninja\Mailers\UserMailer; use App\Ninja\Mailers\ContactMailer; -use App\Events\UserLoggedIn; +use App\Events\UserSignedUp; use App\Events\UserSettingsChanged; use App\Services\AuthService; @@ -100,7 +100,7 @@ class AccountController extends BaseController } Auth::login($user, true); - event(new UserLoggedIn()); + event(new UserSignedUp()); $redirectTo = Input::get('redirect_to') ?: 'invoices/create'; @@ -151,7 +151,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) { @@ -414,7 +414,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 : ''; @@ -430,6 +430,8 @@ class AccountController extends BaseController $data = [ 'client_view_css' => $css, + 'enable_portal_password' => $account->enable_portal_password, + 'send_portal_password' => $account->send_portal_password, 'title' => trans("texts.client_portal"), 'section' => ACCOUNT_CLIENT_PORTAL, 'account' => $account, @@ -545,7 +547,11 @@ class AccountController extends BaseController $account = Auth::user()->account; $account->client_view_css = $sanitized_css; - $account->enable_client_portal = Input::get('enable_client_portal') ? true : false; + + $account->enable_client_portal = !!Input::get('enable_client_portal'); + $account->enable_portal_password = !!Input::get('enable_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/ClientAuth/AuthController.php b/app/Http/Controllers/ClientAuth/AuthController.php new file mode 100644 index 0000000000..c88c8a4b85 --- /dev/null +++ b/app/Http/Controllers/ClientAuth/AuthController.php @@ -0,0 +1,79 @@ +first(); + if ($invitation && !$invitation->is_deleted) { + $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $client->account; + + $data['hideLogo'] = $account->isWhiteLabel(); + $data['clientViewCSS'] = $account->clientViewCSS(); + $data['clientFontUrl'] = $account->getFontsUrl(); + } + } + + return view('clientauth.login')->with($data); + } + + /** + * Get the needed authorization credentials from the request. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + protected function getCredentials(Request $request) + { + $credentials = $request->only('password'); + $credentials['id'] = null; + + $invitation_key = session('invitation_key'); + if($invitation_key){ + $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); + if ($invitation && !$invitation->is_deleted) { + $credentials['id'] = $invitation->contact_id; + } + } + + return $credentials; + } + + /** + * Validate the user login request. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function validateLogin(Request $request) + { + $this->validate($request, [ + 'password' => 'required', + ]); + } +} diff --git a/app/Http/Controllers/ClientAuth/PasswordController.php b/app/Http/Controllers/ClientAuth/PasswordController.php new file mode 100644 index 0000000000..beefb01612 --- /dev/null +++ b/app/Http/Controllers/ClientAuth/PasswordController.php @@ -0,0 +1,197 @@ +middleware('guest'); + Config::set("auth.defaults.passwords","client"); + } + + public function showLinkRequestForm() + { + $data = array(); + $invitation_key = session('invitation_key'); + if($invitation_key){ + $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); + if ($invitation && !$invitation->is_deleted) { + $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $client->account; + + $data['hideLogo'] = $account->isWhiteLabel(); + $data['clientViewCSS'] = $account->clientViewCSS(); + $data['clientFontUrl'] = $account->getFontsUrl(); + } + } + + return view('clientauth.password')->with($data); + } + + /** + * Send a reset link to the given user. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function sendResetLinkEmail(Request $request) + { + $broker = $this->getBroker(); + + $contact_id = null; + $invitation_key = session('invitation_key'); + if($invitation_key){ + $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); + if ($invitation && !$invitation->is_deleted) { + $contact_id = $invitation->contact_id; + } + } + + $response = Password::broker($broker)->sendResetLink(array('id'=>$contact_id), function (Message $message) { + $message->subject($this->getEmailSubject()); + }); + + switch ($response) { + case Password::RESET_LINK_SENT: + return $this->getSendResetLinkEmailSuccessResponse($response); + + case Password::INVALID_USER: + default: + return $this->getSendResetLinkEmailFailureResponse($response); + } + } + + /** + * Display the password reset view for the given token. + * + * If no token is present, display the link request form. + * + * @param \Illuminate\Http\Request $request + * @param string|null $invitation_key + * @param string|null $token + * @return \Illuminate\Http\Response + */ + public function showResetForm(Request $request, $invitation_key = null, $token = null) + { + if (is_null($token)) { + return $this->getEmail(); + } + + $data = compact('token', 'invitation_key'); + $invitation_key = session('invitation_key'); + if($invitation_key){ + $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); + if ($invitation && !$invitation->is_deleted) { + $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $client->account; + + $data['hideLogo'] = $account->isWhiteLabel(); + $data['clientViewCSS'] = $account->clientViewCSS(); + $data['clientFontUrl'] = $account->getFontsUrl(); + } + } + + return view('clientauth.reset')->with($data); + } + + + + /** + * Display the password reset view for the given token. + * + * If no token is present, display the link request form. + * + * @param \Illuminate\Http\Request $request + * @param string|null $invitation_key + * @param string|null $token + * @return \Illuminate\Http\Response + */ + public function getReset(Request $request, $invitation_key = null, $token = null) + { + return $this->showResetForm($request, $invitation_key, $token); + } + + /** + * Reset the given user's password. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function reset(Request $request) + { + $this->validate($request, $this->getResetValidationRules()); + + $credentials = $request->only( + 'password', 'password_confirmation', 'token' + ); + + $credentials['id'] = null; + + $invitation_key = $request->input('invitation_key'); + if($invitation_key){ + $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); + if ($invitation && !$invitation->is_deleted) { + $credentials['id'] = $invitation->contact_id; + } + } + + $broker = $this->getBroker(); + + $response = Password::broker($broker)->reset($credentials, function ($user, $password) { + $this->resetPassword($user, $password); + }); + + switch ($response) { + case Password::PASSWORD_RESET: + return $this->getResetSuccessResponse($response); + + default: + return $this->getResetFailureResponse($request, $response); + } + } + + /** + * Get the password reset validation rules. + * + * @return array + */ + protected function getResetValidationRules() + { + return [ + 'token' => 'required', + 'password' => 'required|confirmed|min:6', + ]; + } +} diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 5632e7de4e..63e370cf41 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -21,11 +21,15 @@ class ApiCheck { */ public function handle($request, Closure $next) { - $loggingIn = $request->is('api/v1/login'); + $loggingIn = $request->is('api/v1/login') || $request->is('api/v1/register'); $headers = Utils::getApiHeaders(); if ($loggingIn) { - // do nothing + // check API secret + if ( ! $request->api_secret || ! env(API_SECRET) || ! hash_equals($request->api_secret, env(API_SECRET))) { + sleep(ERROR_DELAY); + return Response::json('Invalid secret', 403, $headers); + } } else { // check for a valid token $token = AccountToken::where('token', '=', Request::header('X-Ninja-Token'))->first(['id', 'user_id']); @@ -34,7 +38,7 @@ class ApiCheck { Auth::loginUsingId($token->user_id); Session::set('token_id', $token->id); } else { - sleep(3); + sleep(ERROR_DELAY); return Response::json('Invalid token', 403, $headers); } } diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 1780b299f0..9d5d854944 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -1,28 +1,13 @@ auth = $auth; - } - /** * Handle an incoming request. * @@ -30,9 +15,46 @@ class Authenticate { * @param \Closure $next * @return mixed */ - public function handle($request, Closure $next) + public function handle($request, Closure $next, $guard = 'user') { - if ($this->auth->guest()) + $authenticated = Auth::guard($guard)->check(); + + if($guard == 'client' && !empty($request->invitation_key)){ + $old_key = session('invitation_key'); + if($old_key && $old_key != $request->invitation_key){ + if($this->getInvitationContactId($old_key) != $this->getInvitationContactId($request->invitation_key)){ + // This is a different client; reauthenticate + $authenticated = false; + Auth::guard($guard)->logout(); + } + } + Session::put('invitation_key', $request->invitation_key); + } + + if($guard=='client'){ + $invitation_key = session('invitation_key'); + $account_id = $this->getInvitationAccountId($invitation_key); + + if(Auth::guard('user')->check() && Auth::user('user')->account_id === $account_id){ + // This is an admin; let them pretend to be a client + $authenticated = true; + } + + // Does this account require portal passwords? + $account = Account::whereId($account_id)->first(); + if(!$account->enable_portal_password || !$account->isPro()){ + $authenticated = true; + } + + if(!$authenticated){ + $contact = Contact::whereId($this->getInvitationContactId($invitation_key))->first(); + if($contact && !$contact->password){ + $authenticated = true; + } + } + } + + if (!$authenticated) { if ($request->ajax()) { @@ -40,11 +62,30 @@ class Authenticate { } else { - return redirect()->guest('/login'); + return redirect()->guest($guard=='client'?'/client/login':'/login'); } } return $next($request); } - + + protected function getInvitation($key){ + $invitation = Invitation::where('invitation_key', '=', $key)->first(); + if ($invitation && !$invitation->is_deleted) { + return $invitation; + } + else return null; + } + + protected function getInvitationContactId($key){ + $invitation = $this->getInvitation($key); + + return $invitation?$invitation->contact_id:null; + } + + protected function getInvitationAccountId($key){ + $invitation = $this->getInvitation($key); + + return $invitation?$invitation->account_id:null; + } } diff --git a/app/Http/Requests/RegisterRequest.php b/app/Http/Requests/RegisterRequest.php new file mode 100644 index 0000000000..8709d42cc0 --- /dev/null +++ b/app/Http/Requests/RegisterRequest.php @@ -0,0 +1,35 @@ + 'required|unique:users', + 'first_name' => 'required', + 'last_name' => 'required', + 'password' => 'required', + ]; + + return $rules; + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 0e695c6fe5..0c9aef7c1a 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -35,17 +35,20 @@ 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:client'], 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')); @@ -76,6 +79,15 @@ Route::get('/password/reset/{token}', array('as' => 'forgot', 'uses' => 'Auth\Pa Route::post('/password/reset', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@postReset')); Route::get('/user/confirm/{code}', 'UserController@confirm'); +// Client auth +Route::get('/client/login', array('as' => 'login', 'uses' => 'ClientAuth\AuthController@getLogin')); +Route::post('/client/login', array('as' => 'login', 'uses' => 'ClientAuth\AuthController@postLogin')); +Route::get('/client/logout', array('as' => 'logout', 'uses' => 'ClientAuth\AuthController@getLogout')); +Route::get('/client/forgot', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getEmail')); +Route::post('/client/forgot', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postEmail')); +Route::get('/client/password/reset/{invitation_key}/{token}', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getReset')); +Route::post('/client/password/reset', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postReset')); + if (Utils::isNinja()) { Route::post('/signup/register', 'AccountController@doRegister'); @@ -87,7 +99,7 @@ if (Utils::isReseller()) { Route::post('/reseller_stats', 'AppController@stats'); } -Route::group(['middleware' => 'auth'], function() { +Route::group(['middleware' => 'auth:user'], function() { Route::get('dashboard', 'DashboardController@index'); Route::get('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible'); Route::get('hide_message', 'HomeController@hideMessage'); @@ -215,6 +227,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() { Route::get('ping', 'ClientApiController@ping'); Route::post('login', 'AccountApiController@login'); + Route::post('register', 'AccountApiController@register'); Route::get('static', 'AccountApiController@getStaticData'); Route::get('accounts', 'AccountApiController@show'); Route::put('accounts', 'AccountApiController@update'); diff --git a/app/Models/Client.php b/app/Models/Client.php index 61194fe93c..9223264711 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/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 87e3c82c02..e526300a5d 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -27,6 +27,7 @@ class ContactMailer extends Mailer 'firstName', 'invoice', 'quote', + 'password', 'viewLink', 'viewButton', 'paymentLink', @@ -109,6 +110,13 @@ class ContactMailer extends Mailer 'invitation' => $invitation, 'amount' => $invoice->getRequestedAmount() ]; + + if (empty($invitation->contact->password) && $account->isPro() && $account->enable_portal_password && $account->send_portal_password) { + // The contact needs a password + $variables['password'] = $password = $this->generatePassword(); + $invitation->contact->password = bcrypt($password); + $invitation->contact->save(); + } $data = [ 'body' => $this->processVariables($body, $variables), @@ -143,6 +151,28 @@ class ContactMailer extends Mailer return $response; } } + + protected function generatePassword($length = 9) + { + $sets = array( + 'abcdefghjkmnpqrstuvwxyz', + 'ABCDEFGHJKMNPQRSTUVWXYZ', + '23456789', + ); + $all = ''; + $password = ''; + foreach($sets as $set) + { + $password .= $set[array_rand(str_split($set))]; + $all .= $set; + } + $all = str_split($all); + for($i = 0; $i < $length - count($sets); $i++) + $password .= $all[array_rand($all)]; + $password = str_shuffle($password); + + return $password; + } public function sendPaymentConfirmation(Payment $payment) { @@ -232,6 +262,7 @@ class ContactMailer extends Mailer $client = $data['client']; $invitation = $data['invitation']; $invoice = $invitation->invoice; + $passwordHTML = isset($data['password'])?'
'.trans('texts.password').': '.$data['password'].'
':false; $variables = [ '$footer' => $account->getEmailFooter(), @@ -245,10 +276,11 @@ class ContactMailer extends Mailer '$invoice' => $invoice->invoice_number, '$quote' => $invoice->invoice_number, '$link' => $invitation->getLink(), - '$viewLink' => $invitation->getLink(), - '$viewButton' => Form::emailViewButton($invitation->getLink(), $invoice->getEntityType()), - '$paymentLink' => $invitation->getLink('payment'), - '$paymentButton' => Form::emailPaymentButton($invitation->getLink('payment')), + '$password' => $passwordHTML, + '$viewLink' => $invitation->getLink().'$password', + '$viewButton' => Form::emailViewButton($invitation->getLink(), $invoice->getEntityType()).'$password', + '$paymentLink' => $invitation->getLink('payment').'$password', + '$paymentButton' => Form::emailPaymentButton($invitation->getLink('payment')).'$password', '$customClient1' => $account->custom_client_label1, '$customClient2' => $account->custom_client_label2, '$customInvoice1' => $account->custom_invoice_text_label1, @@ -259,11 +291,22 @@ class ContactMailer extends Mailer foreach (Gateway::$paymentTypes as $type) { $camelType = Gateway::getPaymentTypeName($type); $type = Utils::toSnakeCase($camelType); - $variables["\${$camelType}Link"] = $invitation->getLink() . "/{$type}"; + $variables["\${$camelType}Link"] = $invitation->getLink('payment') . "/{$type}"; $variables["\${$camelType}Button"] = Form::emailPaymentButton($invitation->getLink('payment') . "/{$type}"); } - + + $includesPasswordPlaceholder = strpos($template, '$password') !== false; + $str = str_replace(array_keys($variables), array_values($variables), $template); + + if(!$includesPasswordPlaceholder && $passwordHTML){ + $pos = strrpos($str, '$password'); + if($pos !== false) + { + $str = substr_replace($str, $passwordHTML, $pos, 9/* length of "$password" */); + } + } + $str = str_replace('$password', '', $str); $str = autolink($str, 100); return $str; diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index c215f69a97..ff85efb76e 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -59,7 +59,7 @@ class AccountRepository } $user->confirmed = !Utils::isNinja(); - $user->registered = !Utils::isNinja() && $user->email; + $user->registered = !Utils::isNinja() || $email; if (!$user->confirmed) { $user->confirmation_code = str_random(RANDOM_KEY_LENGTH); diff --git a/config/auth.php b/config/auth.php index 6bb5e47c78..7b08fd4731 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,7 +1,6 @@ [ - 'guard' => 'web', + 'guard' => 'user', 'passwords' => 'users', ], @@ -36,10 +35,15 @@ return [ */ 'guards' => [ - 'web' => [ + 'user' => [ 'driver' => 'session', 'provider' => 'users', ], + + 'client' => [ + 'driver' => 'session', + 'provider' => 'client', + ], 'api' => [ 'driver' => 'token', @@ -69,11 +73,11 @@ return [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], - - // 'users' => [ - // 'driver' => 'database', - // 'table' => 'users', - // ], + + 'client' => [ + 'driver' => 'eloquent', + 'model' => App\Models\Contact::class, + ] ], /* @@ -98,7 +102,13 @@ return [ 'passwords' => [ 'users' => [ 'provider' => 'users', - 'email' => 'emails.password', //auth.emails.password + 'email' => 'emails.password', + 'table' => 'password_resets', + 'expire' => 60, + ], + 'client' => [ + 'provider' => 'client', + 'email' => 'emails.client_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..49fcae7fcb --- /dev/null +++ b/database/migrations/2016_02_25_152948_add_client_password.php @@ -0,0 +1,46 @@ +boolean('enable_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('send_portal_password'); + }); + + Schema::table('contacts', function ($table) { + $table->dropColumn('password'); + $table->dropColumn('confirmation_code'); + $table->dropColumn('remember_token'); + }); + } + +} diff --git a/database/seeds/ConstantsSeeder.php b/database/seeds/ConstantsSeeder.php index ec2a94d491..e027416843 100644 --- a/database/seeds/ConstantsSeeder.php +++ b/database/seeds/ConstantsSeeder.php @@ -177,7 +177,7 @@ class ConstantsSeeder extends Seeder 'America/Santiago' => "(GMT-04:00) Santiago", 'Canada/Newfoundland' => "(GMT-03:30) Newfoundland", 'America/Buenos_Aires' => "(GMT-03:00) Buenos Aires", - 'Greenland' => "(GMT-03:00) Greenland", + 'America/Godthab' => "(GMT-03:00) Greenland", 'Atlantic/Stanley' => "(GMT-02:00) Stanley", 'Atlantic/Azores' => "(GMT-01:00) Azores", 'Atlantic/Cape_Verde' => "(GMT-01:00) Cape Verde Is.", diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index c330dbd894..1e83a7821b 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1051,7 +1051,12 @@ $LANG = array( 'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices. The default value is \'R\'.', 'enable_client_portal' => 'Dashboard', 'enable_client_portal_help' => 'Show/hide the dashboard page in the client portal.', - + + // Client Passwords + 'enable_portal_password'=>'Password protect invoices', + 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', + 'send_portal_password'=>'Generate password automatically', + 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', ); return $LANG; diff --git a/resources/views/accounts/client_portal.blade.php b/resources/views/accounts/client_portal.blade.php index 88962e4d0d..745574ad72 100644 --- a/resources/views/accounts/client_portal.blade.php +++ b/resources/views/accounts/client_portal.blade.php @@ -1,69 +1,86 @@ @extends('header') @section('head') - @parent +@parent - + @stop @section('content') - @parent - - {!! Former::open_for_files() - ->addClass('warn-on-exit') !!} +@parent - {!! Former::populateField('enable_client_portal', intval($account->enable_client_portal)) !!} - {!! Former::populateField('client_view_css', $client_view_css) !!} +{!! Former::open_for_files() +->addClass('warn-on-exit') !!} - @if (!Utils::isNinja() && !Auth::user()->account->isWhiteLabel()) -
'.trans('texts.password').': 6h2NWNdw6
':'' !!}";
var vals = [
{!! json_encode($emailFooter) !!},
"{{ $account->getDisplayName() }}",
@@ -211,10 +212,11 @@
"First Name",
"0001",
"0001",
- "{{ URL::to('/view/...') }}",
- '{!! Form::flatButton('view_invoice', '#0b4d78') !!}',
- "{{ URL::to('/payment/...') }}",
- '{!! Form::flatButton('pay_now', '#36c157') !!}',
+ passwordHtml,
+ "{{ URL::to('/view/...') }}$password",
+ '{!! Form::flatButton('view_invoice', '#0b4d78') !!}$password',
+ "{{ URL::to('/payment/...') }}$password",
+ '{!! Form::flatButton('pay_now', '#36c157') !!}$password',
];
// Add blanks for custom values
@@ -230,10 +232,18 @@
{!! "vals.push('" . Form::flatButton('pay_now', '#36c157') . "');" !!}
@endforeach
+ var includesPasswordPlaceholder = str.indexOf('$password') != -1;
+
for (var i=0; i
+ {!! Former::password('password')->placeholder(trans('texts.password'))->raw() !!}
+ {!! Former::hidden('remember')->raw() !!}
+ {!! Button::success(trans('texts.login'))
+ ->withAttributes(['id' => 'loginButton'])
+ ->large()->submit()->block() !!}
+ {!! link_to('/client/forgot', trans('texts.forgot_password')) !!}
+ {!! Button::success(trans('texts.send_email'))->large()->submit()->block() !!}
+ {!! Former::password('password')->placeholder(trans('texts.password'))->raw() !!}
+ {!! Former::password('password_confirmation')->placeholder(trans('texts.confirm_password'))->raw() !!}
+
+ {!! Button::success(trans('texts.save'))->large()->submit()->block() !!}Invoice Ninja | {{ trans('texts.account_login') }}
+
+ @else
+ {{ trans('texts.account_login') }}
+ @endif
+ Invoice Ninja | {{ trans('texts.password_recovery') }}
+
+ @else
+ {{ trans('texts.password_recovery') }}
+ @endif
+ Invoice Ninja | {{ trans('texts.set_password') }}
+
+ @else
+ {{ trans('texts.set_password') }}
+ @endif
+
+ {{ trans('texts.email_from') }}
+