mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-08 20:22:42 +01:00
Add protected download route with signed route signatures
This commit is contained in:
parent
71523ecde3
commit
fb37fc40a3
@ -11,10 +11,12 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Requests\Export\StoreExportRequest;
|
use Illuminate\Support\Str;
|
||||||
use App\Jobs\Company\CompanyExport;
|
|
||||||
use App\Utils\Traits\MakesHash;
|
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use App\Jobs\Company\CompanyExport;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use App\Http\Requests\Export\StoreExportRequest;
|
||||||
|
|
||||||
class ExportController extends BaseController
|
class ExportController extends BaseController
|
||||||
{
|
{
|
||||||
@ -54,8 +56,12 @@ class ExportController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function index(StoreExportRequest $request)
|
public function index(StoreExportRequest $request)
|
||||||
{
|
{
|
||||||
CompanyExport::dispatch(auth()->user()->getCompany(), auth()->user());
|
$hash = Str::uuid();
|
||||||
|
$url = \Illuminate\Support\Facades\URL::temporarySignedRoute('protected_download', now()->addHour(), ['hash' => $hash]);
|
||||||
|
Cache::put($hash, $url, now()->addHour());
|
||||||
|
|
||||||
return response()->json(['message' => 'Processing'], 200);
|
CompanyExport::dispatch(auth()->user()->getCompany(), auth()->user(), $hash);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Processing', 'url' => $url], 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
app/Http/Controllers/ProtectedDownloadController.php
Normal file
41
app/Http/Controllers/ProtectedDownloadController.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Jobs\Util\UnlinkFile;
|
||||||
|
use App\Exceptions\SystemError;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class ProtectedDownloadController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
$hashed_path = Cache::pull($request->hash);
|
||||||
|
|
||||||
|
if (!$hashed_path) {
|
||||||
|
throw new SystemError('File no longer available', 404);
|
||||||
|
abort(404, 'File no longer available');
|
||||||
|
}
|
||||||
|
|
||||||
|
UnlinkFile::dispatch(config('filesystems.default'), $hashed_path)->delay(now()->addSeconds(10));
|
||||||
|
|
||||||
|
return response()->streamDownload(function () use ($hashed_path) {
|
||||||
|
echo Storage::get($hashed_path);
|
||||||
|
}, basename($hashed_path), []);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -41,6 +41,7 @@ use App\Http\Middleware\VerifyCsrfToken;
|
|||||||
use App\Http\Middleware\ContactTokenAuth;
|
use App\Http\Middleware\ContactTokenAuth;
|
||||||
use Illuminate\Auth\Middleware\Authorize;
|
use Illuminate\Auth\Middleware\Authorize;
|
||||||
use App\Http\Middleware\SetDbByCompanyKey;
|
use App\Http\Middleware\SetDbByCompanyKey;
|
||||||
|
use App\Http\Middleware\ValidateSignature;
|
||||||
use App\Http\Middleware\PasswordProtection;
|
use App\Http\Middleware\PasswordProtection;
|
||||||
use App\Http\Middleware\ClientPortalEnabled;
|
use App\Http\Middleware\ClientPortalEnabled;
|
||||||
use App\Http\Middleware\CheckClientExistence;
|
use App\Http\Middleware\CheckClientExistence;
|
||||||
@ -49,16 +50,13 @@ use Illuminate\Http\Middleware\SetCacheHeaders;
|
|||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use App\Http\Middleware\CheckForMaintenanceMode;
|
use App\Http\Middleware\CheckForMaintenanceMode;
|
||||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
|
||||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||||
use Illuminate\Routing\Middleware\ValidateSignature;
|
|
||||||
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
|
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
|
||||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
||||||
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||||
use Illuminate\Routing\Middleware\ThrottleRequestsWithRedis;
|
|
||||||
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
||||||
|
|
||||||
class Kernel extends HttpKernel
|
class Kernel extends HttpKernel
|
||||||
|
49
app/Http/Middleware/ValidateSignature.php
Normal file
49
app/Http/Middleware/ValidateSignature.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Routing\Exceptions\InvalidSignatureException;
|
||||||
|
|
||||||
|
class ValidateSignature
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The names of the parameters that should be ignored.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
protected $ignore = [
|
||||||
|
'q'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @param string|null $relative
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Routing\Exceptions\InvalidSignatureException
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next, $relative = null)
|
||||||
|
{
|
||||||
|
$ignore = property_exists($this, 'except') ? $this->except : $this->ignore;
|
||||||
|
|
||||||
|
if ($request->hasValidSignatureWhileIgnoring($ignore, $relative !== 'relative')) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidSignatureException;
|
||||||
|
}
|
||||||
|
}
|
@ -11,39 +11,39 @@
|
|||||||
|
|
||||||
namespace App\Jobs\Company;
|
namespace App\Jobs\Company;
|
||||||
|
|
||||||
use App\Jobs\Mail\NinjaMailerJob;
|
|
||||||
use App\Jobs\Mail\NinjaMailerObject;
|
|
||||||
use App\Libraries\MultiDB;
|
|
||||||
use App\Mail\DownloadBackup;
|
|
||||||
use App\Models\Company;
|
|
||||||
use App\Models\CreditInvitation;
|
|
||||||
use App\Models\InvoiceInvitation;
|
|
||||||
use App\Models\PurchaseOrderInvitation;
|
|
||||||
use App\Models\QuoteInvitation;
|
|
||||||
use App\Models\RecurringInvoiceInvitation;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\VendorContact;
|
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Models\Company;
|
||||||
|
use App\Libraries\MultiDB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use App\Mail\DownloadBackup;
|
||||||
|
use App\Jobs\Util\UnlinkFile;
|
||||||
|
use App\Models\VendorContact;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
|
use App\Models\QuoteInvitation;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use App\Models\CreditInvitation;
|
||||||
|
use App\Jobs\Mail\NinjaMailerJob;
|
||||||
|
use App\Models\InvoiceInvitation;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use App\Jobs\Mail\NinjaMailerObject;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Models\PurchaseOrderInvitation;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use App\Models\RecurringInvoiceInvitation;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\App;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
|
|
||||||
class CompanyExport implements ShouldQueue
|
class CompanyExport implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
|
||||||
|
|
||||||
public $company;
|
private $export_format = 'json';
|
||||||
|
|
||||||
private $export_format;
|
|
||||||
|
|
||||||
private $export_data = [];
|
private $export_data = [];
|
||||||
|
|
||||||
public $user;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new job instance.
|
* Create a new job instance.
|
||||||
@ -52,11 +52,8 @@ class CompanyExport implements ShouldQueue
|
|||||||
* @param User $user
|
* @param User $user
|
||||||
* @param string $custom_token_name
|
* @param string $custom_token_name
|
||||||
*/
|
*/
|
||||||
public function __construct(Company $company, User $user, $export_format = 'json')
|
public function __construct(public Company $company, private User $user, public string $hash)
|
||||||
{
|
{
|
||||||
$this->company = $company;
|
|
||||||
$this->user = $user;
|
|
||||||
$this->export_format = $export_format;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -467,6 +464,11 @@ class CompanyExport implements ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
$storage_file_path = Storage::disk(config('filesystems.default'))->url('backups/'.$file_name);
|
$storage_file_path = Storage::disk(config('filesystems.default'))->url('backups/'.$file_name);
|
||||||
|
$storage_path = Storage::disk(config('filesystems.default'))->path('backups/'.$file_name);
|
||||||
|
|
||||||
|
|
||||||
|
$url = Cache::get($this->hash);
|
||||||
|
Cache::put($this->hash, $storage_path, now()->addHour());
|
||||||
|
|
||||||
App::forgetInstance('translator');
|
App::forgetInstance('translator');
|
||||||
$t = app('translator');
|
$t = app('translator');
|
||||||
@ -475,13 +477,15 @@ class CompanyExport implements ShouldQueue
|
|||||||
$company_reference = Company::find($this->company->id);
|
$company_reference = Company::find($this->company->id);
|
||||||
|
|
||||||
$nmo = new NinjaMailerObject;
|
$nmo = new NinjaMailerObject;
|
||||||
$nmo->mailable = new DownloadBackup($storage_file_path, $company_reference);
|
$nmo->mailable = new DownloadBackup($url, $company_reference);
|
||||||
$nmo->to_user = $this->user;
|
$nmo->to_user = $this->user;
|
||||||
$nmo->company = $company_reference;
|
$nmo->company = $company_reference;
|
||||||
$nmo->settings = $this->company->settings;
|
$nmo->settings = $this->company->settings;
|
||||||
|
|
||||||
NinjaMailerJob::dispatch($nmo, true);
|
NinjaMailerJob::dispatch($nmo, true);
|
||||||
|
|
||||||
|
UnlinkFile::dispatch(config('filesystems.default'), $storage_path)->delay(now()->addHours(1));
|
||||||
|
|
||||||
if (Ninja::isHosted()) {
|
if (Ninja::isHosted()) {
|
||||||
sleep(3);
|
sleep(3);
|
||||||
unlink($zip_path);
|
unlink($zip_path);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@extends('portal.ninja2020.layout.error')
|
@extends('portal.ninja2020.layout.error')
|
||||||
|
|
||||||
@section('title', __($title) ?? 'Server Error')
|
@section('title', $title ?? 'Error')
|
||||||
@section('code', __($code) ?? '500')
|
@section('code', __($code) ?? '500')
|
||||||
@section('message', __($message) ?? 'System Error')
|
@section('message', __($message) ?? 'System Error')
|
||||||
|
@ -101,6 +101,7 @@ use App\Http\Controllers\Support\Messages\SendingController;
|
|||||||
use App\Http\Controllers\Reports\ClientSalesReportController;
|
use App\Http\Controllers\Reports\ClientSalesReportController;
|
||||||
use App\Http\Controllers\Reports\InvoiceItemReportController;
|
use App\Http\Controllers\Reports\InvoiceItemReportController;
|
||||||
use App\Http\Controllers\PaymentNotificationWebhookController;
|
use App\Http\Controllers\PaymentNotificationWebhookController;
|
||||||
|
use App\Http\Controllers\ProtectedDownloadController;
|
||||||
use App\Http\Controllers\Reports\ProductSalesReportController;
|
use App\Http\Controllers\Reports\ProductSalesReportController;
|
||||||
use App\Http\Controllers\Reports\ClientBalanceReportController;
|
use App\Http\Controllers\Reports\ClientBalanceReportController;
|
||||||
use App\Http\Controllers\Reports\ClientContactReportController;
|
use App\Http\Controllers\Reports\ClientContactReportController;
|
||||||
@ -401,4 +402,6 @@ Route::post('api/v1/yodlee/data_updates', [YodleeController::class, 'dataUpdates
|
|||||||
Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1');
|
Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1');
|
||||||
Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1');
|
Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1');
|
||||||
|
|
||||||
|
Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('signed')->middleware('throttle:300,1');
|
||||||
|
|
||||||
Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404');
|
Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404');
|
Loading…
Reference in New Issue
Block a user