1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 20:22:42 +01:00

Merge branch 'develop' of github.com:hillelcoren/invoice-ninja into develop

This commit is contained in:
Hillel Coren 2016-03-08 19:32:36 +02:00
commit 1fb74a0355
20 changed files with 970 additions and 118 deletions

View File

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

View File

@ -0,0 +1,79 @@
<?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()
{
$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.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',
]);
}
}

View File

@ -0,0 +1,197 @@
<?php namespace App\Http\Controllers\ClientAuth;
use Config;
use App\Http\Controllers\Controller;
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()
{
$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',
];
}
}

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,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;
}
}

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: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 +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');
@ -87,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');
@ -683,4 +694,4 @@ if (Utils::isNinjaDev())
//ini_set('memory_limit','1024M');
//Auth::loginUsingId(1);
}
*/
*/

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

@ -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'])?'<p>'.trans('texts.password').': '.$data['password'].'<p>':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,
@ -262,9 +294,21 @@ class ContactMailer extends Mailer
$variables["\${$camelType}Link"] = $invitation->getLink() . "/{$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);
$str = autolink($str, 100);
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);
return $str;
}

View File

@ -1,7 +1,6 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
@ -14,7 +13,7 @@ return [
*/
'defaults' => [
'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,
],

View File

@ -0,0 +1,46 @@
<?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('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');
});
}
}

View File

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

View File

@ -1,69 +1,86 @@
@extends('header')
@section('head')
@parent
@parent
<link href='https://fonts.googleapis.com/css?family=Roboto+Mono' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Roboto+Mono' rel='stylesheet' type='text/css'>
@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())
<div class="alert alert-warning" style="font-size:larger;">
<center>
{!! trans('texts.white_label_custom_css', ['link'=>'<a href="#" onclick="$(\'#whiteLabelModal\').modal(\'show\');">'.trans('texts.white_label_purchase_link').'</a>']) !!}
</center>
</div>
@endif
{!! Former::populateField('enable_client_portal', intval($account->enable_client_portal)) !!}
{!! Former::populateField('client_view_css', $client_view_css) !!}
{!! Former::populateField('enable_portal_password', $enable_portal_password) !!}
{!! Former::populateField('send_portal_password', $send_portal_password) !!}
@include('accounts.nav', ['selected' => ACCOUNT_CLIENT_PORTAL])
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.client_portal') !!}</h3>
</div>
<div class="panel-body">
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('enable_client_portal')
->text(trans('texts.enable'))
->help(trans('texts.enable_client_portal_help')) !!}
</div>
</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>
@if (!Utils::isNinja() && !Auth::user()->account->isWhiteLabel())
<div class="alert alert-warning" style="font-size:larger;">
<center>
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
{!! trans('texts.white_label_custom_css', ['link'=>'<a href="#" onclick="$(\'#whiteLabelModal\').modal(\'show\');">'.trans('texts.white_label_purchase_link').'</a>']) !!}
</center>
</div>
@endif
{!! Former::close() !!}
@include('accounts.nav', ['selected' => ACCOUNT_CLIENT_PORTAL])
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.client_portal') !!}</h3>
</div>
<div class="panel-body">
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('enable_client_portal')
->text(trans('texts.enable'))
->help(trans('texts.enable_client_portal_help')) !!}
</div>
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('enable_portal_password')
->text(trans('texts.enable_portal_password'))
->help(trans('texts.enable_portal_password_help'))
->label('&nbsp;') !!}
</div>
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('send_portal_password')
->text(trans('texts.send_portal_password'))
->help(trans('texts.send_portal_password_help'))
->label('&nbsp;') !!}
</div>
</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>
<center>
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
</center>
{!! Former::close() !!}
<script>
$('#enable_portal_password').change(fixCheckboxes);
function fixCheckboxes(){
var checked = $('#enable_portal_password').is(':checked');
$('#send_portal_password').prop('disabled', !checked);
}
fixCheckboxes();
</script>
@stop

View File

@ -200,6 +200,7 @@
}
var keys = {!! json_encode(\App\Ninja\Mailers\ContactMailer::$variableFields) !!};
var passwordHtml = "{!! $account->isPro() && $account->enable_portal_password && $account->send_portal_password?'<p>'.trans('texts.password').': 6h2NWNdw6<p>':'' !!}";
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<keys.length; i++) {
var regExp = new RegExp('\\$'+keys[i], 'g');
str = str.replace(regExp, vals[i]);
}
if(!includesPasswordPlaceholder){
var lastSpot = str.lastIndexOf('$password')
str = str.slice(0, lastSpot) + str.slice(lastSpot).replace('$password', passwordHtml);
}
str = str.replace(/\$password/g,'');
return str;
}

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,120 @@
@extends('public.header')
@section('head')
@parent
<style type="text/css">
body {
padding-top: 40px;
padding-bottom: 40px;
}
.modal-header {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
background:#222;
color:#fff
}
.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">
@if (!isset($hideLogo) || !$hideLogo)
<a href="{{ NINJA_WEB_URL }}" target="_blank">
<img src="{{ asset('images/icon-login.png') }}" />
<h4>Invoice Ninja | {{ trans('texts.account_login') }}</h4>
</a>
@else
<h4>{{ trans('texts.account_login') }}</h4>
@endif
</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,108 @@
@extends('public.header')
@section('head')
@parent
<style type="text/css">
body {
padding-top: 40px;
padding-bottom: 40px;
}
.modal-header {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
background:#222;
color:#fff
}
.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">
@if (!isset($hideLogo) || !$hideLogo)
<a href="{{ NINJA_WEB_URL }}" target="_blank">
<img src="{{ asset('images/icon-login.png') }}" />
<h4>Invoice Ninja | {{ trans('texts.password_recovery') }}</h4>
</a>
@else
<h4>{{ trans('texts.password_recovery') }}</h4>
@endif
</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,111 @@
@extends('public.header')
@section('head')
@parent
<style type="text/css">
body {
padding-top: 40px;
padding-bottom: 40px;
}
.modal-header {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
background:#222;
color:#fff
}
.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">
@if (!isset($hideLogo) || !$hideLogo)
<a href="{{ NINJA_WEB_URL }}" target="_blank">
<img src="{{ asset('images/icon-login.png') }}" />
<h4>Invoice Ninja | {{ trans('texts.set_password') }}</h4>
</a>
@else
<h4>{{ trans('texts.set_password') }}</h4>
@endif
</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,10 @@
attr: {name: 'contacts[' + \$index() + '][email]', id:'email'+\$index()}") !!}
{!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown',
attr: {name: 'contacts[' + \$index() + '][phone]'}") !!}
@if ($account->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

@ -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]'}") !!}
@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">