1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 12:42:36 +01:00

Finalize basic client portal password support

This commit is contained in:
Joshua Dwire 2016-03-04 22:22:54 -05:00
parent 903904d423
commit 793ba76415
14 changed files with 680 additions and 210 deletions

View File

@ -1,127 +0,0 @@
<?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

@ -0,0 +1,62 @@
<?php namespace App\Http\Controllers\ClientAuth;
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 App\Models\Invitation;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class AuthController extends Controller {
protected $guard = 'client';
protected $redirectTo = '/client/dashboard';
use AuthenticatesUsers;
public function showLoginForm()
{
return view('clientauth.login');
}
/**
* 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',
]);
}
}

View File

@ -0,0 +1,168 @@
<?php namespace App\Http\Controllers\ClientAuth;
use Config;
use App\Http\Controllers\Controller;
use App\Http\Brokers\ClientPasswordBroker;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Password;
use App\Models\Invitation;
class PasswordController extends Controller {
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
protected $redirectTo = '/client/dashboard';
/**
* Create a new password controller instance.
*
* @param \Illuminate\Contracts\Auth\Guard $auth
* @param \Illuminate\Contracts\Auth\PasswordBroker $passwords
* @return void
*/
public function __construct()
{
$this->middleware('guest');
Config::set("auth.defaults.passwords","client");
}
public function showLinkRequestForm()
{
return view('clientauth.password');
}
/**
* 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();
}
return view('clientauth.reset')->with(compact('token', 'invitation_key'));
}
/**
* 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',
];
}
}

View File

@ -1,28 +1,13 @@
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Auth;
use Session;
use App\Models\Invitation;
use App\Models\Contact;
use App\Models\Account;
class Authenticate {
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
@ -30,9 +15,45 @@ 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'){
if(Auth::guard('user')->check()){
// This is an admin; let them pretend to be a client
$authenticated = true;
}
// Does this account require portal passwords?
$invitation_key = session('invitation_key');
$account = Account::whereId($this->getInvitationAccountId($invitation_key))->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 +61,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;
}
}

View File

@ -35,7 +35,7 @@ Route::get('/keep_alive', 'HomeController@keepAlive');
Route::post('/get_started', 'AccountController@getStarted');
// Client visible pages
Route::group(['middleware' => 'auth'], function() {
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');
@ -78,6 +78,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');
@ -89,7 +98,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');
@ -685,4 +694,4 @@ if (Utils::isNinjaDev())
//ini_set('memory_limit','1024M');
//Auth::loginUsingId(1);
}
*/
*/

View File

@ -1,46 +0,0 @@
<?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

@ -13,7 +13,7 @@ return [
*/
'defaults' => [
'guard' => 'web',
'guard' => 'user',
'passwords' => 'users',
],
@ -35,10 +35,15 @@ return [
*/
'guards' => [
'web' => [
'user' => [
'driver' => 'session',
'provider' => 'users',
],
'client' => [
'driver' => 'session',
'provider' => 'client',
],
'api' => [
'driver' => 'token',
@ -68,11 +73,11 @@ return [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
'client' => [
'driver' => 'eloquent',
'model' => App\Models\Contact::class,
]
],
/*
@ -97,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,
],

View File

@ -32,6 +32,9 @@
{!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown'") !!}
{!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown'") !!}
{!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown'") !!}
@if ($account->isPro() && $account->enable_portal_password)
{!! Former::password('password')->data_bind("value: password()?'-%unchanged%-':'', valueUpdate: 'afterkeydown'") !!}
@endif
<div class="form-group">
<div class="col-lg-8 col-lg-offset-4">

View File

@ -0,0 +1,116 @@
@extends('master')
@section('head')
<link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('css/style.css') }}" rel="stylesheet" type="text/css"/>
<style type="text/css">
body {
padding-top: 40px;
padding-bottom: 40px;
}
.modal-header {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.modal-header h4 {
margin:0;
}
.modal-header img {
float: left;
margin-right: 20px;
}
.form-signin {
max-width: 400px;
margin: 0 auto;
background: #fff;
}
p.link a {
font-size: 11px;
}
.form-signin .inner {
padding: 20px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
margin-bottom: 17px !important;
}
.form-signin .form-control:focus {
z-index: 2;
}
.modal-header a:link,
.modal-header a:visited,
.modal-header a:hover,
.modal-header a:active {
text-decoration: none;
color: white;
}
</style>
@endsection
@section('body')
<div class="container">
@include('partials.warn_session', ['redirectTo' => '/client/login'])
{!! Former::open('client/login')
->rules(['password' => 'required'])
->addClass('form-signin') !!}
{{ Former::populateField('remember', 'true') }}
<div class="modal-header">
<a href="{{ NINJA_WEB_URL }}" target="_blank">
<h4>{{ trans('texts.account_login') }}</h4>
</a>
</div>
<div class="inner">
<p>
{!! Former::password('password')->placeholder(trans('texts.password'))->raw() !!}
{!! Former::hidden('remember')->raw() !!}
</p>
<p>{!! Button::success(trans('texts.login'))
->withAttributes(['id' => 'loginButton'])
->large()->submit()->block() !!}</p>
<p class="link">
{!! link_to('/client/forgot', trans('texts.forgot_password')) !!}
</p>
@if (count($errors->all()))
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</div>
@endif
@if (Session::has('warning'))
<div class="alert alert-warning">{{ Session::get('warning') }}</div>
@endif
@if (Session::has('message'))
<div class="alert alert-info">{{ Session::get('message') }}</div>
@endif
@if (Session::has('error'))
<div class="alert alert-danger"><li>{{ Session::get('error') }}</li></div>
@endif
</div>
{!! Former::close() !!}
</div>
@endsection

View File

@ -0,0 +1,101 @@
@extends('master')
@section('head')
<link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('css/style.css') }}" rel="stylesheet" type="text/css"/>
<style type="text/css">
body {
padding-top: 40px;
padding-bottom: 40px;
}
.modal-header {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.modal-header h4 {
margin:0;
}
.modal-header img {
float: left;
margin-right: 20px;
}
.form-signin {
max-width: 400px;
margin: 0 auto;
background: #fff;
}
p.link a {
font-size: 11px;
}
.form-signin .inner {
padding: 20px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
margin-bottom: 17px !important;
}
.form-signin .form-control:focus {
z-index: 2;
}
</style>
@stop
@section('body')
<div class="container">
{!! Former::open('client/forgot')->addClass('form-signin') !!}
<div class="modal-header">
<h4>{{ trans('texts.password_recovery') }}</h4></div>
<div class="inner">
<p>{!! Button::success(trans('texts.send_email'))->large()->submit()->block() !!}</p>
@if (count($errors->all()))
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</div>
@endif
@if (session('status'))
<div class="alert alert-info">
{{ session('status') }}
</div>
@endif
<!-- if there are login errors, show them here -->
@if (Session::has('warning'))
<div class="alert alert-warning">{{ Session::get('warning') }}</div>
@endif
@if (Session::has('message'))
<div class="alert alert-info">{{ Session::get('message') }}</div>
@endif
@if (Session::has('error'))
<div class="alert alert-danger">{{ Session::get('error') }}</div>
@endif
{!! Former::close() !!}
</div>
</div>
<script type="text/javascript">
$(function() {
$('#email').focus();
})
</script>
@stop

View File

@ -0,0 +1,104 @@
@extends('master')
@section('head')
<link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('css/style.css') }}" rel="stylesheet" type="text/css"/>
<style type="text/css">
body {
padding-top: 40px;
padding-bottom: 40px;
}
.modal-header {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.modal-header h4 {
margin:0;
}
.modal-header img {
float: left;
margin-right: 20px;
}
.form-signin {
max-width: 400px;
margin: 0 auto;
background: #fff;
}
p.link a {
font-size: 11px;
}
.form-signin .inner {
padding: 20px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
margin-bottom: 17px !important;
}
.form-signin .form-control:focus {
z-index: 2;
}
</style>
@stop
@section('body')
<div class="container">
{!! Former::open('/client/password/reset')->addClass('form-signin')->rules(array(
'password' => 'required',
'password_confirmation' => 'required',
)) !!}
<div class="modal-header">
<h4>{{ trans('texts.set_password') }}</h4></div>
<div class="inner">
<input type="hidden" name="token" value="{{{ $token }}}">
<input type="hidden" name="invitation_key" value="{{{ $invitation_key }}}">
<p>
{!! Former::password('password')->placeholder(trans('texts.password'))->raw() !!}
{!! Former::password('password_confirmation')->placeholder(trans('texts.confirm_password'))->raw() !!}
</p>
<p>{!! Button::success(trans('texts.save'))->large()->submit()->block() !!}</p>
@if (count($errors->all()))
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</div>
@endif
<!-- if there are login errors, show them here -->
@if (Session::has('warning'))
<div class="alert alert-warning">{{ Session::get('warning') }}</div>
@endif
@if (Session::has('message'))
<div class="alert alert-info">{{ Session::get('message') }}</div>
@endif
@if (Session::has('error'))
<div class="alert alert-danger">{{ Session::get('error') }}</div>
@endif
{!! Former::close() !!}
</div>
</div>
@stop

View File

@ -93,7 +93,7 @@
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)
@if ($account->isPro() && $account->enable_portal_password)
{!! Former::password('password')->data_bind("value: password()?'-%unchanged%-':'', valueUpdate: 'afterkeydown',
attr: {name: 'contacts[' + \$index() + '][password]'}") !!}
@endif

View File

@ -0,0 +1,26 @@
@extends('emails.master_user')
@section('body')
<div>
{{ trans('texts.reset_password') }}
</div>
&nbsp;
<div>
<center>
@include('partials.email_button', [
'link' => URL::to("client/password/reset/".session('invitation_key')."/{$token}"),
'field' => 'reset',
'color' => '#36c157',
])
</center>
</div>
&nbsp;
<div>
{{ trans('texts.email_signature') }}<br/>
{{ trans('texts.email_from') }}
</div>
&nbsp;
<div>
{{ trans('texts.reset_password_footer') }}
</div>
@stop

View File

@ -543,7 +543,10 @@
->addClass('client-email') !!}
{!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][phone]'}") !!}
f
@if ($account->isPro() && $account->enable_portal_password)
{!! Former::password('password')->data_bind("value: (typeof password=='function'?password():null)?'-%unchanged%-':'', valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][password]'}") !!}
@endif
<div class="form-group">
<div class="col-lg-8 col-lg-offset-4">
<span class="redlink bold" data-bind="visible: $parent.contacts().length > 1">