diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 6bc0b80316cf..b5afc076f092 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -663,6 +663,7 @@ class CompanySettings extends BaseSettings '$task.line_total', ], 'total_columns' => [ + '$net_subtotal', '$subtotal', '$discount', '$custom_surcharge1', diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index a1960438fcb6..efd75e38415c 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -16,6 +16,7 @@ use App\DataMapper\Analytics\LoginSuccess; use App\Events\User\UserLoggedIn; use App\Http\Controllers\BaseController; use App\Http\Controllers\Controller; +use App\Http\Requests\Login\LoginRequest; use App\Jobs\Account\CreateAccount; use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Util\SystemLogger; @@ -156,7 +157,7 @@ class LoginController extends BaseController * ), * ) */ - public function apiLogin(Request $request) + public function apiLogin(LoginRequest $request) { $this->forced_includes = ['company_users']; diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 913ce2d9564b..e77fa41e7e74 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -25,7 +25,10 @@ use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyToken; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\Ninja\RefundCancelledAccount; +use App\Mail\Company\CompanyDeleted; use App\Models\Account; use App\Models\Company; use App\Models\CompanyUser; @@ -504,6 +507,15 @@ class CompanyController extends BaseController $company->company_users->each(function ($company_user){ $company_user->forceDelete(); }); + + $other_company = $company->account->companies->where('id', '!=', $company->id)->first(); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new CompanyDeleted($company->present()->name, auth()->user(), $company->account, $company->settings); + $nmo->company = $other_company; + $nmo->settings = $other_company->settings; + $nmo->to_user = auth()->user(); + NinjaMailerJob::dispatch($nmo); $company->delete(); diff --git a/app/Http/Middleware/PasswordProtection.php b/app/Http/Middleware/PasswordProtection.php index e3a80cf45730..54163fac473c 100644 --- a/app/Http/Middleware/PasswordProtection.php +++ b/app/Http/Middleware/PasswordProtection.php @@ -44,6 +44,14 @@ class PasswordProtection else $timeout = $timeout/1000; + //test if password if base64 encoded + $x_api_password = $request->header('X-API-PASSWORD'); + + if($request->header('X-API-PASSWORD-BASE64')) + { + $x_api_password = base64_decode($request->header('X-API-PASSWORD-BASE64')); + } + if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in')) { Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); @@ -66,7 +74,7 @@ class PasswordProtection ]; //If OAuth and user also has a password set - check both - if ($existing_user = MultiDB::hasUser($query) && auth()->user()->company()->oauth_password_required && auth()->user()->has_password && Hash::check(auth()->user()->password, $request->header('X-API-PASSWORD'))) { + if ($existing_user = MultiDB::hasUser($query) && auth()->user()->company()->oauth_password_required && auth()->user()->has_password && Hash::check(auth()->user()->password, $x_api_password)) { nlog("existing user with password"); @@ -86,7 +94,7 @@ class PasswordProtection return response()->json($error, 412); - }elseif ($request->header('X-API-PASSWORD') && Hash::check($request->header('X-API-PASSWORD'), auth()->user()->password)) { + }elseif ($x_api_password && Hash::check($x_api_password, auth()->user()->password)) { Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); diff --git a/app/Http/Requests/Login/LoginRequest.php b/app/Http/Requests/Login/LoginRequest.php new file mode 100644 index 000000000000..bb8db25bd0a5 --- /dev/null +++ b/app/Http/Requests/Login/LoginRequest.php @@ -0,0 +1,54 @@ + 'required', + 'password' => 'required', + ]; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + // if(base64_decode(base64_encode($input['password'])) === $input['password']) + // $input['password'] = base64_decode($input['password']); + + // nlog($input['password']); + + $this->replace($input); + } +} diff --git a/app/Listeners/Misc/InvitationViewedListener.php b/app/Listeners/Misc/InvitationViewedListener.php index 4df1f36209ce..e77b0d08296e 100644 --- a/app/Listeners/Misc/InvitationViewedListener.php +++ b/app/Listeners/Misc/InvitationViewedListener.php @@ -47,8 +47,6 @@ class InvitationViewedListener implements ShouldQueue $entity_name = lcfirst(class_basename($event->entity)); $invitation = $event->invitation; - // $notification = new EntityViewedNotification($invitation, $entity_name); - $nmo = new NinjaMailerObject; $nmo->mailable = new NinjaMailer( (new EntityViewedObject($invitation, $entity_name))->build() ); $nmo->company = $invitation->company; @@ -57,8 +55,9 @@ class InvitationViewedListener implements ShouldQueue foreach ($invitation->company->company_users as $company_user) { $entity_viewed = "{$entity_name}_viewed"; + $entity_viewed_all = "{$entity_name}_viewed_all"; - $methods = $this->findUserNotificationTypes($invitation, $company_user, $entity_name, ['all_notifications', $entity_viewed]); + $methods = $this->findUserNotificationTypes($invitation, $company_user, $entity_name, ['all_notifications', $entity_viewed, $entity_viewed_all]); if (($key = array_search('mail', $methods)) !== false) { unset($methods[$key]); @@ -68,16 +67,7 @@ class InvitationViewedListener implements ShouldQueue } - // $notification->method = $methods; - - // $company_user->user->notify($notification); } - // if (isset($invitation->company->slack_webhook_url)) { - // $notification->method = ['slack']; - - // Notification::route('slack', $invitation->company->slack_webhook_url) - // ->notify($notification); - // } } } diff --git a/app/Listeners/SendVerificationNotification.php b/app/Listeners/SendVerificationNotification.php index b64c2e0d03ca..99e095b10d60 100644 --- a/app/Listeners/SendVerificationNotification.php +++ b/app/Listeners/SendVerificationNotification.php @@ -16,6 +16,7 @@ use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerObject; use App\Libraries\MultiDB; use App\Mail\Admin\VerifyUserObject; +use App\Mail\User\UserAdded; use App\Notifications\Ninja\VerifyUser; use App\Utils\Ninja; use Exception; @@ -52,5 +53,13 @@ class SendVerificationNotification implements ShouldQueue $event->user->service()->invite($event->company); + $nmo = new NinjaMailerObject; + $nmo->mailable = new UserAdded($event->company, $event->creating_user, $event->user); + $nmo->company = $event->company; + $nmo->settings = $event->company->settings; + $nmo->to_user = $event->creating_user; + NinjaMailerJob::dispatch($nmo); + + } } diff --git a/app/Mail/Company/CompanyDeleted.php b/app/Mail/Company/CompanyDeleted.php new file mode 100644 index 000000000000..da021afeaf29 --- /dev/null +++ b/app/Mail/Company/CompanyDeleted.php @@ -0,0 +1,61 @@ +company = $company; + $this->user = $user; + $this->account = $account; + $this->settings = $settings; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->from(config('mail.from.address'), config('mail.from.name')) + ->subject(ctrans('texts.company_deleted')) + ->view('email.admin.company_deleted') + ->with([ + 'settings' => $this->settings, + 'logo' => '', + 'title' => ctrans('texts.company_deleted'), + 'body' => ctrans('texts.company_deleted_body', ['company' => $this->company, 'user' => $this->user->present()->name(), 'time' => now()]), + 'whitelabel' => $this->account->isPaid(), + ]); + } +} diff --git a/app/Mail/User/UserAdded.php b/app/Mail/User/UserAdded.php new file mode 100644 index 000000000000..2c07fbbcb8a7 --- /dev/null +++ b/app/Mail/User/UserAdded.php @@ -0,0 +1,59 @@ +company = $company; + $this->user = $user; + $this->created_user = $created_user; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->from(config('mail.from.address'), config('mail.from.name')) + ->subject(ctrans('texts.created_user')) + ->view('email.admin.user_added') + ->with([ + 'settings' => $this->company->settings, + 'logo' => $this->company->present()->logo(), + 'title' => ctrans('texts.created_user'), + 'body' => ctrans('texts.user_created_user', ['user' => $this->user->present()->name(), 'created_user' => $this->created_user->present()->name(), 'time' => now()]), + 'whitelabel' => $this->company->account->isPaid(), + ]); + } +} diff --git a/app/Services/Invoice/UpdateReminder.php b/app/Services/Invoice/UpdateReminder.php index dd7142179931..de45f5c98adc 100644 --- a/app/Services/Invoice/UpdateReminder.php +++ b/app/Services/Invoice/UpdateReminder.php @@ -48,7 +48,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder1_sent) && $this->settings->schedule_reminder1 == 'after_invoice_date' && - $this->settings->num_days_reminder1 > 0) { + $this->settings->enable_reminder1) { $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder1)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -57,7 +57,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder1_sent) && $this->settings->schedule_reminder1 == 'before_due_date' && - $this->settings->num_days_reminder1 > 0) { + $this->settings->enable_reminder1) { $reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder1)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -66,7 +66,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder1_sent) && $this->settings->schedule_reminder1 == 'after_due_date' && - $this->settings->num_days_reminder1 > 0) { + $this->settings->enable_reminder1) { $reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder1)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -75,7 +75,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder2_sent) && $this->settings->schedule_reminder2 == 'after_invoice_date' && - $this->settings->num_days_reminder2 > 0) { + $this->settings->enable_reminder2) { $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder2)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -84,7 +84,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder2_sent) && $this->settings->schedule_reminder2 == 'before_due_date' && - $this->settings->num_days_reminder2 > 0) { + $this->settings->enable_reminder2) { $reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder2)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -93,7 +93,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder2_sent) && $this->settings->schedule_reminder2 == 'after_due_date' && - $this->settings->num_days_reminder2 > 0) { + $this->settings->enable_reminder2) { $reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder2)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -102,7 +102,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder3_sent) && $this->settings->schedule_reminder3 == 'after_invoice_date' && - $this->settings->num_days_reminder3 > 0) { + $this->settings->enable_reminder3) { $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder3)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -111,7 +111,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder3_sent) && $this->settings->schedule_reminder3 == 'before_due_date' && - $this->settings->num_days_reminder3 > 0) { + $this->settings->enable_reminder3) { $reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->subDays($this->settings->num_days_reminder3)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -120,7 +120,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder3_sent) && $this->settings->schedule_reminder3 == 'after_due_date' && - $this->settings->num_days_reminder3 > 0) { + $this->settings->enable_reminder3) { $reminder_date = Carbon::parse($this->invoice->due_date)->startOfDay()->addDays($this->settings->num_days_reminder3)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); @@ -128,7 +128,7 @@ class UpdateReminder extends AbstractService } if ($this->invoice->last_sent_date && - (int)$this->settings->endless_reminder_frequency_id > 0) { + $this->settings->enable_reminder_endless) { $reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int)$this->settings->endless_reminder_frequency_id)->addSeconds($offset); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); diff --git a/app/Transformers/DesignTransformer.php b/app/Transformers/DesignTransformer.php index c73d9cc8fc52..1271ed55ef7f 100644 --- a/app/Transformers/DesignTransformer.php +++ b/app/Transformers/DesignTransformer.php @@ -51,6 +51,7 @@ class DesignTransformer extends EntityTransformer 'archived_at' => (int) $design->deleted_at, 'created_at' => (int) $design->created_at, 'is_deleted' => (bool) $design->is_deleted, + 'is_free' => ($design->id <= 4) ? true : false, ]; } } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 277b5adff860..f50fd27d5c4c 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -167,6 +167,7 @@ class HtmlEngine $data['$invoice.discount'] = ['value' => Number::formatMoney($this->entity_calc->getTotalDiscount(), $this->client) ?: ' ', 'label' => ctrans('texts.discount')]; $data['$discount'] = &$data['$invoice.discount']; $data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->client) ?: ' ', 'label' => ctrans('texts.subtotal')]; + $data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes), $this->client) ?: ' ', 'label' => ctrans('texts.subtotal')]; $data['$invoice.subtotal'] = &$data['$subtotal']; if ($this->entity->partial > 0) { diff --git a/app/Utils/Ninja.php b/app/Utils/Ninja.php index 1a31a11daf57..17c859b6f6c6 100644 --- a/app/Utils/Ninja.php +++ b/app/Utils/Ninja.php @@ -170,4 +170,29 @@ class Ninja // return implode('-', $parts); // } + // + + /* + * Available - but not recommended for use + * + * This will guarantee a given string IS the correct format for a + * base64 encoded string , + * but can't guarantee that it is a base64 encoded string + * + */ + public static function isBase64Encoded(string $s) : bool + { + + // Check if there are valid base64 characters + if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $s)) return false; + // Decode the string in strict mode and check the results + $decoded = base64_decode($s, true); + if(false === $decoded) return false; + // if string returned contains not printable chars + if (0 < preg_match('/((?![[:graph:]])(?!\s)(?!\p{L}))./', $decoded, $matched)) return false; + // Encode the string again + if(base64_encode($decoded) != $s) return false; + return true; + + } } diff --git a/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index cb87d238a52b..b98d5dd3f539 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -83,6 +83,7 @@ class SystemHealth 'flutter_renderer' => (string)config('ninja.flutter_canvas_kit'), 'jobs_pending' => (int) Queue::size(), 'pdf_engine' => (string) self::getPdfEngine(), + 'queue' => (string) config('queue.default'), ]; } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 2bff23717e28..fc01b693ddaf 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4281,6 +4281,9 @@ $LANG = array( 'quotes_with_status_sent_can_be_approved' => 'Only quotes with "Sent" status can be approved.', 'no_quotes_available_for_download' => 'No quotes available for download.', 'copyright' => 'Copyright', + 'user_created_user' => ':user created :created_user at :time', + 'company_deleted' => 'Company deleted', + 'company_deleted_body' => 'Company [ :company ] was deleted by :user', ); return $LANG; diff --git a/resources/views/email/admin/company_deleted.blade.php b/resources/views/email/admin/company_deleted.blade.php new file mode 100644 index 000000000000..2281b8f1bdbe --- /dev/null +++ b/resources/views/email/admin/company_deleted.blade.php @@ -0,0 +1,6 @@ +@component('email.template.admin', ['logo' => $logo, 'settings' => $settings]) +
+

{!! $title !!}

+

{!! $body !!}

+
+@endcomponent diff --git a/resources/views/email/admin/user_added.blade.php b/resources/views/email/admin/user_added.blade.php new file mode 100644 index 000000000000..2281b8f1bdbe --- /dev/null +++ b/resources/views/email/admin/user_added.blade.php @@ -0,0 +1,6 @@ +@component('email.template.admin', ['logo' => $logo, 'settings' => $settings]) +
+

{!! $title !!}

+

{!! $body !!}

+
+@endcomponent diff --git a/tests/Unit/Base64Test.php b/tests/Unit/Base64Test.php new file mode 100644 index 000000000000..7aed2415f033 --- /dev/null +++ b/tests/Unit/Base64Test.php @@ -0,0 +1,57 @@ +assertFalse(Ninja::isBase64Encoded('x')); + } + + public function testCorrectBase64Encoding() + { + $this->assertTrue(Ninja::isBase64Encoded('MTIzNDU2')); + } + + public function testBadBase64StringScenaro1() + { + $this->assertFalse(Ninja::isBase64Encoded('Matthies')); + } + + public function testBadBase64StringScenaro2() + { + $this->assertFalse(Ninja::isBase64Encoded('Barthels')); + } + + public function testBadBase64StringScenaro3() + { + $this->assertFalse(Ninja::isBase64Encoded('aaa')); + } + +} \ No newline at end of file