Merge pull request #6876 from turbo124/v5-develop

Refactor for backup storage location
This commit is contained in:
David Bomba 2021-10-21 08:20:11 +11:00 committed by GitHub
commit 5579cee330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 440 additions and 33 deletions

View File

@ -0,0 +1,91 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Libraries\MultiDB;
use App\Models\Backup;
use App\Models\Design;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use stdClass;
class BackupUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:backup-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Shift backups from DB to storage';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//always return state to first DB
$current_db = config('database.default');
if (! config('ninja.db.multi_db_enabled')) {
$this->handleOnDb();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->handleOnDb();
}
MultiDB::setDB($current_db);
}
}
private function handleOnDb()
{
Backup::whereHas('activity')->whereNotNull('html_backup')->cursor()->each(function($backup){
if($backup->activity->client()->exists()){
$client = $backup->activity->client;
$backup->storeRemotely($backup->html_backup, $client);
}
});
}
}

View File

@ -126,15 +126,13 @@ class CompanySettings extends BaseSettings
public $auto_bill = 'off'; //off,always,optin,optout //@implemented
public $auto_bill_date = 'on_due_date'; // on_due_date , on_send_date //@implemented
//public $design = 'views/pdf/design1.blade.php'; //@deprecated - never used
public $invoice_terms = ''; //@implemented
public $quote_terms = ''; //@implemented
public $invoice_taxes = 0; // ? used in AP only?
// public $enabled_item_tax_rates = 0;
public $invoice_design_id = 'VolejRejNm'; //@implemented
public $quote_design_id = 'VolejRejNm'; //@implemented
public $credit_design_id = 'VolejRejNm'; //@implemented
public $invoice_design_id = 'Wpmbk5ezJn'; //@implemented
public $quote_design_id = 'Wpmbk5ezJn'; //@implemented
public $credit_design_id = 'Wpmbk5ezJn'; //@implemented
public $invoice_footer = ''; //@implemented
public $credit_footer = ''; //@implemented
public $credit_terms = ''; //@implemented
@ -146,7 +144,6 @@ class CompanySettings extends BaseSettings
public $tax_name3 = ''; //@TODO where do we use this?
public $tax_rate3 = 0; //@TODO where do we use this?
public $payment_type_id = '0'; //@TODO where do we use this?
// public $invoice_fields = ''; //@TODO is this redundant, we store this in the custom_fields on the company?
public $valid_until = ''; //@implemented

View File

@ -20,6 +20,7 @@ use App\Utils\Traits\Pdf\PdfMaker;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use stdClass;
@ -136,19 +137,33 @@ class ActivityController extends BaseController
public function downloadHistoricalEntity(DownloadHistoricalEntityRequest $request, Activity $activity)
{
$backup = $activity->backup;
$html_backup = '';
if (! $backup || ! $backup->html_backup) {
/* Refactor 20-10-2021
*
* We have moved the backups out of the database and into object storage.
* In order to handle edge cases, we still check for the database backup
* in case the file no longer exists
*/
if($backup && $backup->filename && Storage::disk(config('filesystems.default'))->exists($backup->filename)){ //disk
$html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->path($backup->filename));
}
elseif($backup && $backup->html_backup){ //db
$html_backup = $backup->html_backup;
}
elseif (! $backup || ! $backup->html_backup) { //failed
return response()->json(['message'=> ctrans('texts.no_backup_exists'), 'errors' => new stdClass], 404);
}
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom)->convertHtmlToPdf($backup->html_backup);
$pdf = (new Phantom)->convertHtmlToPdf($html_backup);
}
elseif(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($backup->html_backup);
$pdf = (new NinjaPdf())->build($html_backup);
}
else {
$pdf = $this->makePdf(null, null, $backup->html_backup);
$pdf = $this->makePdf(null, null, $html_backup);
}
if (isset($activity->invoice_id)) {

View File

@ -738,6 +738,10 @@ class BaseController extends Controller
return redirect()->secure(request()->getRequestUri());
}
/* Clean up URLs and remove query parameters from the URL*/
if(request()->has('login') && request()->input('login') == 'true')
return redirect('/')->with(['login' => "true"]);
$data = [];
//pass report errors bool to front end
@ -748,6 +752,9 @@ class BaseController extends Controller
$data['build'] = request()->has('build') ? request()->input('build') : '';
$data['login'] = request()->has('login') ? request()->input('login') : "false";
if(request()->session()->has('login'))
$data['login'] = "true";
$data['user_agent'] = request()->server('HTTP_USER_AGENT');
$data['path'] = $this->setBuild();

View File

@ -18,6 +18,8 @@ use App\Http\Requests\GroupSetting\EditGroupSettingRequest;
use App\Http\Requests\GroupSetting\ShowGroupSettingRequest;
use App\Http\Requests\GroupSetting\StoreGroupSettingRequest;
use App\Http\Requests\GroupSetting\UpdateGroupSettingRequest;
use App\Http\Requests\GroupSetting\UploadGroupSettingRequest;
use App\Models\Account;
use App\Models\GroupSetting;
use App\Repositories\GroupSettingRepository;
use App\Transformers\GroupSettingTransformer;
@ -497,4 +499,68 @@ class GroupSettingController extends BaseController
return $this->listResponse(GroupSetting::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
}
/**
* Update the specified resource in storage.
*
* @param UploadGroupSettingRequest $request
* @param GroupSetting $group_setting
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/group_settings/{id}/upload",
* operationId="uploadGroupSetting",
* tags={"group_settings"},
* summary="Uploads a document to a group setting",
* description="Handles the uploading of a document to a group setting",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Group Setting Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Group Setting object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Invoice"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadGroupSettingRequest $request, GroupSetting $group_setting)
{
if(!$this->checkFeature(Account::FEATURE_DOCUMENTS))
return $this->featureFailure();
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $group_setting);
return $this->itemResponse($group_setting->fresh());
}
}

View File

@ -180,6 +180,25 @@ class MigrationController extends BaseController
$company->vendors()->forceDelete();
$company->expenses()->forceDelete();
$settings = $company->settings;
/* Reset all counters to 1 after a purge */
$settings->recurring_invoice_number_counter = 1;
$settings->invoice_number_counter = 1;
$settings->quote_number_counter = 1;
$settings->client_number_counter = 1;
$settings->credit_number_counter = 1;
$settings->task_number_counter = 1;
$settings->expense_number_counter = 1;
$settings->recurring_expense_number_counter = 1;
$settings->recurring_quote_number_counter = 1;
$settings->vendor_number_counter = 1;
$settings->ticket_number_counter = 1;
$settings->payment_number_counter = 1;
$settings->project_number_counter = 1;
$company->settings = $settings;
$company->save();
return response()->json(['message' => 'Settings preserved'], 200);

View File

@ -0,0 +1,47 @@
<?php
/**
* @OA\Schema(
* schema="RecurringExpense",
* type="object",
* @OA\Property(property="id", type="string", example="Opnel5aKBz", description="_________"),
* @OA\Property(property="user_id", type="string", example="", description="__________"),
* @OA\Property(property="assigned_user_id", type="string", example="", description="__________"),
* @OA\Property(property="company_id", type="string", example="", description="________"),
* @OA\Property(property="client_id", type="string", example="", description="________"),
* @OA\Property(property="invoice_id", type="string", example="", description="________"),
* @OA\Property(property="bank_id", type="string", example="", description="________"),
* @OA\Property(property="invoice_currency_id", type="string", example="", description="________"),
* @OA\Property(property="expense_currency_id", type="string", example="", description="________"),
* @OA\Property(property="invoice_category_id", type="string", example="", description="________"),
* @OA\Property(property="payment_type_id", type="string", example="", description="________"),
* @OA\Property(property="recurring_expense_id", type="string", example="", description="________"),
* @OA\Property(property="private_notes", type="string", example="", description="________"),
* @OA\Property(property="public_notes", type="string", example="", description="________"),
* @OA\Property(property="transaction_reference", type="string", example="", description="________"),
* @OA\Property(property="transcation_id", type="string", example="", description="________"),
* @OA\Property(property="custom_value1", type="string", example="", description="________"),
* @OA\Property(property="custom_value2", type="string", example="", description="________"),
* @OA\Property(property="custom_value3", type="string", example="", description="________"),
* @OA\Property(property="custom_value4", type="string", example="", description="________"),
* @OA\Property(property="tax_name1", type="string", example="", description="________"),
* @OA\Property(property="tax_name2", type="string", example="", description="________"),
* @OA\Property(property="tax_rate1", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="tax_rate2", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="tax_name3", type="string", example="", description="________"),
* @OA\Property(property="tax_rate3", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="amount", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="frequency_id", type="number", format="int", example="1", description="_________"),
* @OA\Property(property="remaining_cycles", type="number", format="int", example="1", description="_________"),
* @OA\Property(property="foreign_amount", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="exchange_rate", type="number", format="float", example="0.80", description="_________"),
* @OA\Property(property="date", type="string", example="", description="________"),
* @OA\Property(property="payment_date", type="string", example="", description="________"),
* @OA\Property(property="should_be_invoiced", type="boolean", example=true, description="_________"),
* @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"),
* @OA\Property(property="last_sent_date", type="string", format="date", example="1994-07-30", description="The Date it was sent last"),
* @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The next send date"),
* @OA\Property(property="invoice_documents", type="boolean", example=true, description=""),
* @OA\Property(property="updated_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* @OA\Property(property="archived_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* )
*/

View File

@ -10,8 +10,8 @@
* email="contact@invoiceninja.com"
* ),
* @OA\License(
* name="Attribution Assurance License",
* url="https://opensource.org/licenses/AAL"
* name="Elastic License",
* url="https://www.elastic.co/licensing/elastic-license"
* ),
* ),
* @OA\Server(

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\GroupSetting;
use App\Http\Requests\Request;
class UploadGroupSettingRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->group_setting);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -70,6 +70,13 @@ class StoreInvoiceRequest extends Request
$input['amount'] = 0;
$input['balance'] = 0;
if(array_key_exists('tax_rate1', $input) && is_null($input['tax_rate1']))
$input['tax_rate1'] = 0;
if(array_key_exists('tax_rate2', $input) && is_null($input['tax_rate2']))
$input['tax_rate2'] = 0;
if(array_key_exists('tax_rate3', $input) && is_null($input['tax_rate3']))
$input['tax_rate3'] = 0;
$this->replace($input);
}
}

View File

@ -48,7 +48,7 @@ class StoreUserRequest extends Request
}
if (Ninja::isHosted()) {
$rules['hosted_users'] = new CanAddUserRule(auth()->user()->company()->account);
$rules['id'] = new CanAddUserRule();
}
return $rules;

View File

@ -18,11 +18,9 @@ use Illuminate\Contracts\Validation\Rule;
*/
class CanAddUserRule implements Rule
{
public $account;
public function __construct($account)
public function __construct()
{
$this->account = $account;
}
/**
@ -32,7 +30,7 @@ class CanAddUserRule implements Rule
*/
public function passes($attribute, $value)
{
return $this->account->users->count() < $this->account->num_users;
return auth()->user()->company()->account->users->count() < auth()->user()->company()->account->num_users;
}
/**
@ -40,6 +38,6 @@ class CanAddUserRule implements Rule
*/
public function message()
{
return ctrans('texts.limit_users', ['limit' => $this->account->num_users]);
return ctrans('texts.limit_users', ['limit' => auth()->user()->company()->account->num_users]);
}
}

View File

@ -887,6 +887,7 @@ class CompanyImport implements ShouldQueue
[
'hashed_id',
'company_id',
'backup',
],
[
['users' => 'user_id'],
@ -1192,6 +1193,10 @@ class CompanyImport implements ShouldQueue
}
if(array_key_exists('deleted_at', $obj_array) && $obj_array['deleted_at'] > 1){
$obj_array['deleted_at'] = now();
}
$activity_invitation_key = false;
if($class == 'App\Models\Activity'){
@ -1270,6 +1275,10 @@ class CompanyImport implements ShouldQueue
}
}
if(array_key_exists('deleted_at', $obj_array) && $obj_array['deleted_at'] > 1){
$obj_array['deleted_at'] = now();
}
/* New to convert product ids from old hashes to new hashes*/
if($class == 'App\Models\Subscription'){
$obj_array['product_ids'] = $this->recordProductIds($obj_array['product_ids']);
@ -1320,6 +1329,10 @@ class CompanyImport implements ShouldQueue
}
}
if(array_key_exists('deleted_at', $obj_array) && $obj_array['deleted_at'] > 1){
$obj_array['deleted_at'] = now();
}
/* New to convert product ids from old hashes to new hashes*/
if($class == 'App\Models\Subscription'){
//$obj_array['product_ids'] = $this->recordProductIds($obj_array['product_ids']);

View File

@ -110,6 +110,16 @@ class SendRecurring implements ShouldQueue
$this->recurring_invoice->save();
/*
if ($this->recurring_invoice->company->pause_recurring_until_paid){
$this->recurring_invoice->service()
->stop();
}
*/
//Admin notification for recurring invoice sent.
if ($invoice->invitations->count() >= 1 ) {
$invoice->entityEmailEvent($invoice->invitations->first(), 'invoice', 'email_template_invoice');

View File

@ -11,6 +11,9 @@
namespace App\Models;
use App\Models\Client;
use Illuminate\Support\Facades\Storage;
class Backup extends BaseModel
{
public function getEntityType()
@ -22,4 +25,26 @@ class Backup extends BaseModel
{
return $this->belongsTo(Activity::class);
}
public function storeRemotely(string $html, Client $client)
{
if(strlen($html) == 0)
return;
$path = $client->backup_path() . "/";
$filename = now()->format('Y_m_d'). "_" . md5(time()) . ".html";
$file_path = $path . $filename;
Storage::disk(config('filesystems.default'))->makeDirectory($path, 0775);
Storage::disk(config('filesystems.default'))->put($file_path, $html);
if(Storage::disk(config('filesystems.default'))->exists($file_path)){
$this->html_backup = '';
$this->filename = $file_path;
$this->save();
}
}
}

View File

@ -729,6 +729,12 @@ class Client extends BaseModel implements HasLocalePreference
})->first()->locale;
}
public function backup_path()
{
return $this->company->company_key.'/'.$this->client_hash.'/backups';
}
public function invoice_filepath($invitation)
{
$contact_key = $invitation->contact->contact_key;

View File

@ -73,24 +73,25 @@ class ActivityRepository extends BaseRepository
if ($entity instanceof User || $entity->company->is_disabled)
return;
$backup = new Backup();
if (get_class($entity) == Invoice::class
|| get_class($entity) == Quote::class
|| get_class($entity) == Credit::class
|| get_class($entity) == RecurringInvoice::class
) {
$backup = new Backup();
$entity->load('client');
$contact = $entity->client->primary_contact()->first();
$backup->html_backup = $this->generateHtml($entity);
$backup->amount = $entity->amount;
$backup->activity_id = $activity->id;
$backup->json_backup = '';
$backup->save();
$backup->storeRemotely($this->generateHtml($entity), $entity->client);
}
$backup->activity_id = $activity->id;
$backup->json_backup = '';
$backup->save();
}
public function getTokenId(array $event_vars)
@ -126,7 +127,7 @@ class ActivityRepository extends BaseRepository
if(!$entity->invitations()->exists() || !$design){
nlog("No invitations for entity {$entity->id} - {$entity->number}");
return;
return '';
}
$entity->load('client.company', 'invitations');

View File

@ -483,6 +483,30 @@ class InvoiceService
}
/*
//if paid invoice is attached to a recurring invoice - check if we need to unpause the recurring invoice
if ($this->invoice->status_id == Invoice::STATUS_PAID &&
$this->invoice->recurring_id &&
$this->invoice->company->pause_recurring_until_paid &&
($this->invoice->recurring_invoice->status_id != RecurringInvoice::STATUS_ACTIVE || $this->invoice->recurring_invoice->status_id != RecurringInvoice::STATUS_COMPLETED))
{
$recurring_invoice = $this->invoice->recurring_invoice;
// Check next_send_date if it is in the past - calculate
$next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay();
if(next_send_date->lt(now())){
$recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d'));
$recurring_invoice->save();
}
// Start the recurring invoice
$recurring_invoice->service()
->start();
}
*/
return $this;
}

View File

@ -82,6 +82,9 @@ class MarkPaid extends AbstractService
->updateBalance($payment->amount * -1)
->updatePaidToDate($payment->amount)
->setStatus(Invoice::STATUS_PAID)
->save();
$this->invoice->service()
->applyNumber()
->deletePdf()
->save();
@ -103,7 +106,10 @@ class MarkPaid extends AbstractService
->updatePaidToDate($payment->amount)
->save();
$this->invoice->service()->workFlow()->save();
$this->invoice
->service()
->workFlow()
->save();
return $this->invoice;
}

View File

@ -84,9 +84,19 @@ class SystemHealth
'jobs_pending' => (int) Queue::size(),
'pdf_engine' => (string) self::getPdfEngine(),
'queue' => (string) config('queue.default'),
'trailing_slash' => (bool) self::checkUrlState(),
];
}
public static function checkUrlState()
{
if (env('APP_URL') && substr(env('APP_URL'), -1) == '/')
return true;
return false;
}
public static function getPdfEngine()
{
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja')

View File

@ -13,7 +13,7 @@
"tasks",
"freelancer"
],
"license": "Attribution Assurance License",
"license": "Elastic License",
"authors": [
{
"name": "Hillel Coren",

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddFilenameToBackupsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('backups', function (Blueprint $table) {
$table->text('filename')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@ -88,12 +88,8 @@ class SquareCreditCard {
}
catch(typeError){
console.log(typeError);
die("failed in the catch");
}
// console.log(" verification tokem = " + verificationToken.token);
// verificationToken = verificationResults.token;
console.debug('Verification Token:', verificationToken);
document.querySelector('input[name="verificationToken"]').value =

View File

@ -86,6 +86,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::resource('group_settings', 'GroupSettingController');
Route::post('group_settings/bulk', 'GroupSettingController@bulk');
Route::put('group_settings/{group_setting}/upload', 'GroupSettingController@upload')->name('group_settings.upload');
Route::post('import', 'ImportController@import')->name('import.import');
Route::post('import_json', 'ImportJsonController@import')->name('import.import_json');