mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-05 18:52:44 +01:00
Password protected invoices (#3635)
* Password protected invoices (wip) * Add support for invitations * Update comments & php-cs-fixer * Add Forgot your password
This commit is contained in:
parent
01e8afc1f6
commit
2215f40ec3
114
app/Http/Controllers/ClientPortal/EntityViewController.php
Normal file
114
app/Http/Controllers/ClientPortal/EntityViewController.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\ClientPortal;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class EntityViewController extends Controller
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
/**
|
||||
* Available options for viewing.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $entity_types = ['invoice', 'quote'];
|
||||
|
||||
/**
|
||||
* Show the entity outside client portal.
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function index(string $entity_type, string $invitation_key)
|
||||
{
|
||||
if (!in_array($entity_type, $this->entity_types)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$invitation_entity = sprintf('App\\Models\\%sInvitation', ucfirst($entity_type));
|
||||
|
||||
$key = $entity_type . '_id';
|
||||
|
||||
$invitation = $invitation_entity::whereRaw("BINARY `key`= ?", [$invitation_key])->firstOrFail();
|
||||
|
||||
$contact = $invitation->contact;
|
||||
|
||||
if (is_null($contact->password) || empty($contact->password)) {
|
||||
return redirect("/client/password/reset?email={$contact->email}");
|
||||
}
|
||||
|
||||
$entity_class = sprintf('App\\Models\\%s', ucfirst($entity_type));
|
||||
$entity = $entity_class::findOrFail($invitation->{$key});
|
||||
|
||||
if ((bool) $invitation->contact->client->getSetting('enable_client_portal_password') !== false) {
|
||||
session()->flash("{$entity_type}_VIEW_{$entity->hashed_id}", true);
|
||||
}
|
||||
|
||||
if (!session("{$entity_type}_VIEW_{$entity->hashed_id}")) {
|
||||
return redirect()->route('client.entity_view.password', compact('entity_type', 'invitation_key'));
|
||||
}
|
||||
|
||||
return $this->render('view_entity.index', [
|
||||
'root' => 'themes',
|
||||
'entity' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for entering password.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* @param string $invitation_key
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function password(string $entity_type, string $invitation_key)
|
||||
{
|
||||
return $this->render('view_entity.password', [
|
||||
'root' => 'themes',
|
||||
'entity_type' => $entity_type,
|
||||
]);
|
||||
}
|
||||
|
||||
/**`
|
||||
* Handle the password check.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* @param string $invitation_key
|
||||
*
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse|mixed
|
||||
*/
|
||||
public function handlePassword(string $entity_type, string $invitation_key)
|
||||
{
|
||||
if (!in_array($entity_type, $this->entity_types)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$invitation_entity = sprintf('App\\Models\\%sInvitation', ucfirst($entity_type));
|
||||
|
||||
$key = $entity_type . '_id';
|
||||
|
||||
$invitation = $invitation_entity::whereRaw("BINARY `key`= ?", [$invitation_key])->firstOrFail();
|
||||
|
||||
$contact = $invitation->contact;
|
||||
|
||||
$check = Hash::check(request()->password, $contact->password);
|
||||
|
||||
$entity_class = sprintf('App\\Models\\%s', ucfirst($entity_type));
|
||||
|
||||
$entity = $entity_class::findOrFail($invitation->{$key});
|
||||
|
||||
if ($check) {
|
||||
session()->flash("{$entity_type}_VIEW_{$entity->hashed_id}", true);
|
||||
|
||||
return redirect()->route('client.entity_view', compact('entity_type', 'invitation_key'));
|
||||
}
|
||||
|
||||
session()->flash('PASSWORD_FAILED', true);
|
||||
return back();
|
||||
}
|
||||
}
|
@ -3201,4 +3201,5 @@ return [
|
||||
'page' => 'Page',
|
||||
'of' => 'Of',
|
||||
'view_credit' => 'View Credit',
|
||||
'to_view_entity_password' => 'To view the :entity you need to enter password.',
|
||||
];
|
||||
|
@ -24,7 +24,7 @@
|
||||
<label for="email" class="input-label">{{ ctrans('texts.email_address') }}</label>
|
||||
<input type="email" name="email" id="email"
|
||||
class="input"
|
||||
value="{{ old('email') }}"
|
||||
value="{{ request()->query('email') ?? old('email') }}"
|
||||
autofocus>
|
||||
@error('email')
|
||||
<div class="validation validation-fail">
|
||||
|
58
resources/views/themes/ninja2020/view_entity/index.blade.php
Normal file
58
resources/views/themes/ninja2020/view_entity/index.blade.php
Normal file
@ -0,0 +1,58 @@
|
||||
@extends('portal.ninja2020.layout.clean')
|
||||
|
||||
@push('head')
|
||||
<meta name="pdf-url" content="{{ asset($entity->pdf_file_path()) }}">
|
||||
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>
|
||||
@endpush
|
||||
|
||||
@section('body')
|
||||
<div class="container mx-auto my-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<section class="flex items-center">
|
||||
<button class="input-label" id="previous-page-button">
|
||||
<svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="input-label" id="next-page-button">
|
||||
<svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</section>
|
||||
<div class="flex items-center">
|
||||
@if($entity instanceof App\Models\Invoice)
|
||||
<button class="button button-primary">{{ ctrans('texts.pay_now') }}</button>
|
||||
@elseif($$entity instanceof App\Models\Quote)
|
||||
<button class="button button-primary">{{ ctrans('texts.approve') }}</button>
|
||||
@endif
|
||||
<button class="button button-primary ml-2">{{ ctrans('texts.download') }}</button>
|
||||
<div x-data="{ open: false }" @keydown.escape="open = false" @click.away="open = false" class="relative inline-block text-left ml-2">
|
||||
<div>
|
||||
<button @click="open = !open" class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600">
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
|
||||
<div class="rounded-md bg-white shadow-xs">
|
||||
<div class="py-1">
|
||||
<a target="_blank" href="{{ asset($entity->pdf_file_path()) }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<canvas id="pdf-placeholder" class="shadow-lg border rounded-lg bg-white mt-4 p-4"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('footer')
|
||||
<script src="{{ asset('js/clients/shared/pdf.js') }}"></script>
|
||||
@endsection
|
@ -0,0 +1,31 @@
|
||||
@extends('portal.ninja2020.layout.clean')
|
||||
|
||||
@section('body')
|
||||
<div class="flex h-screen">
|
||||
<div class="m-auto md:w-1/3 lg:w-1/5">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-center text-3xl">{{ ctrans('texts.password') }}</h1>
|
||||
<p class="text-sm text-center text-gray-700">{{ ctrans('texts.to_view_entity_password', ['entity' => $entity_type]) }}</p>
|
||||
<form method="post" class="mt-6">
|
||||
@csrf
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-center">
|
||||
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
|
||||
<a class="text-xs text-gray-600 hover:text-gray-800 ease-in duration-100" href="{{ route('client.password.request') }}">{{ trans('texts.forgot_password') }}</a>
|
||||
</div>
|
||||
<input type="password" name="password" id="password" class="input" autofocus>
|
||||
|
||||
@if(session('PASSWORD_FAILED'))
|
||||
<div class="validation validation-fail">
|
||||
{{ ctrans('auth.failed') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<button class="button button-primary button-block">{{ ctrans('texts.continue') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
@ -12,6 +12,10 @@ Route::post('client/password/email', 'Auth\ContactForgotPasswordController@sendR
|
||||
Route::get('client/password/reset/{token}', 'Auth\ContactResetPasswordController@showResetForm')->name('client.password.reset')->middleware('locale');
|
||||
Route::post('client/password/reset', 'Auth\ContactResetPasswordController@reset')->name('client.password.update')->middleware('locale');
|
||||
|
||||
Route::get('view/{entity_type}/{invitation_key}', 'ClientPortal\EntityViewController@index')->name('client.entity_view');
|
||||
Route::get('view/{entity_type}/{invitation_key}/password', 'ClientPortal\EntityViewController@password')->name('client.entity_view.password');
|
||||
Route::post('view/{entity_type}/{invitation_key}/password', 'ClientPortal\EntityViewController@handlePassword');
|
||||
|
||||
//todo implement domain DB
|
||||
Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', 'as' => 'client.'], function () {
|
||||
Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit
|
||||
|
Loading…
Reference in New Issue
Block a user