diff --git a/app/Exceptions/ClientHostedMigrationException.php b/app/Exceptions/ClientHostedMigrationException.php new file mode 100644 index 000000000000..0f14ee0c7103 --- /dev/null +++ b/app/Exceptions/ClientHostedMigrationException.php @@ -0,0 +1,19 @@ +json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); } - // $cu->first()->account->companies->each(function ($company) use ($cu, $request) { - // if ($company->tokens()->where('is_system', true)->count() == 0) { - // (new CreateCompanyToken($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')))->handle(); - // } - // }); + $cu->first()->account->companies->each(function ($company) use ($cu, $request) { + if ($company->tokens()->where('is_system', true)->count() == 0) { + (new CreateCompanyToken($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')))->handle(); + } + }); if ($request->has('current_company') && $request->input('current_company') == 'true') { $cu->where('company_id', $company_token->company_id); @@ -480,13 +480,13 @@ class LoginController extends BaseController return $cu; } - // if (auth()->user()->company_users()->count() != auth()->user()->tokens()->distinct('company_id')->count()) { - // auth()->user()->companies->each(function ($company) { - // if (!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->exists()) { - // (new CreateCompanyToken($company, auth()->user(), 'Google_O_Auth'))->handle(); - // } - // }); - // } + if (auth()->user()->company_users()->count() != auth()->user()->tokens()->distinct('company_id')->count()) { + auth()->user()->companies->each(function ($company) { + if (!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->exists()) { + (new CreateCompanyToken($company, auth()->user(), 'Google_O_Auth'))->handle(); + } + }); + } $truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $set_company->id)->first()); diff --git a/app/Http/Controllers/BankIntegrationController.php b/app/Http/Controllers/BankIntegrationController.php index 552aa02fe6d7..a742255e320f 100644 --- a/app/Http/Controllers/BankIntegrationController.php +++ b/app/Http/Controllers/BankIntegrationController.php @@ -472,10 +472,12 @@ class BankIntegrationController extends BaseController $ids = request()->input('ids'); - $bank_integrations = BankIntegration::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get(); + $bank_integrations = BankIntegration::withTrashed()->whereIn('id', $this->transformKeys($ids)) + ->company() + ->cursor() + ->each(function ($bank_integration, $key) use ($action) { - $bank_integrations->each(function ($bank_integration, $key) use ($action) { - $this->bank_integration_repo->{$action}($bank_integration); + $this->bank_integration_repo->{$action}($bank_integration); }); /* Need to understand which permission are required for the given bulk action ie. view / edit */ diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 15b9bcd674d2..21e92e494ce4 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -17,6 +17,7 @@ use App\Events\PurchaseOrder\PurchaseOrderWasUpdated; use App\Factory\PurchaseOrderFactory; use App\Filters\PurchaseOrderFilters; use App\Http\Requests\PurchaseOrder\ActionPurchaseOrderRequest; +use App\Http\Requests\PurchaseOrder\BulkPurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\CreatePurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\DestroyPurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\EditPurchaseOrderRequest; @@ -475,12 +476,12 @@ class PurchaseOrderController extends BaseController * ), * ) */ - public function bulk() + public function bulk(BulkPurchaseOrderRequest $request) { - $action = request()->input('action'); + $action = $request->input('action'); - $ids = request()->input('ids'); + $ids = $request->input('ids'); if(Ninja::isHosted() && (stripos($action, 'email') !== false) && !auth()->user()->company()->account->account_sms_verified) return response(['message' => 'Please verify your account to send emails.'], 400); @@ -497,7 +498,6 @@ class PurchaseOrderController extends BaseController if ($action == 'bulk_download' && $purchase_orders->count() >= 1) { $purchase_orders->each(function ($purchase_order) { if (auth()->user()->cannot('view', $purchase_order)) { - nlog("access denied"); return response()->json(['message' => ctrans('text.access_denied')]); } }); diff --git a/app/Http/Controllers/TaskStatusController.php b/app/Http/Controllers/TaskStatusController.php index c037cfb05973..f487de5ad877 100644 --- a/app/Http/Controllers/TaskStatusController.php +++ b/app/Http/Controllers/TaskStatusController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Factory\TaskStatusFactory; use App\Filters\TaskStatusFilters; +use App\Http\Requests\TaskStatus\ActionTaskStatusRequest; use App\Http\Requests\TaskStatus\CreateTaskStatusRequest; use App\Http\Requests\TaskStatus\DestroyTaskStatusRequest; use App\Http\Requests\TaskStatus\ShowTaskStatusRequest; @@ -449,18 +450,20 @@ class TaskStatusController extends BaseController * ), * ) */ - public function bulk() + public function bulk(ActionTaskStatusRequest $request) { - $action = request()->input('action'); + $action = $request->input('action'); - $ids = request()->input('ids'); + $ids = $request->input('ids'); - $task_status = TaskStatus::withTrashed()->company()->find($this->transformKeys($ids)); + TaskStatus::withTrashed() + ->company() + ->whereIn('id', $this->transformKeys($ids)) + ->cursor() + ->each(function ($task_status, $key) use ($action) { - $task_status->each(function ($task_status, $key) use ($action) { - if (auth()->user()->can('edit', $task_status)) { $this->task_status_repo->{$action}($task_status); - } + }); return $this->listResponse(TaskStatus::withTrashed()->whereIn('id', $this->transformKeys($ids))); diff --git a/app/Http/Requests/PurchaseOrder/ActionPurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/ActionPurchaseOrderRequest.php index 36cd952c15ee..63fe16244090 100644 --- a/app/Http/Requests/PurchaseOrder/ActionPurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/ActionPurchaseOrderRequest.php @@ -12,21 +12,16 @@ namespace App\Http\Requests\PurchaseOrder; use App\Http\Requests\Request; -use App\Models\PurchaseOrder; -use App\Utils\Traits\MakesHash; class ActionPurchaseOrderRequest extends Request { - use MakesHash; + private $error_msg; /** * Determine if the user is authorized to make this request. * * @return bool */ - private $error_msg; - - // private $invoice; public function authorize() : bool { diff --git a/app/Http/Requests/PurchaseOrder/BulkPurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/BulkPurchaseOrderRequest.php new file mode 100644 index 000000000000..0790444a1a49 --- /dev/null +++ b/app/Http/Requests/PurchaseOrder/BulkPurchaseOrderRequest.php @@ -0,0 +1,38 @@ + 'required|bail|array|min:1', + 'action' => 'in:archive,restore,delete,email,bulk_download,bulk_print,mark_sent,download,send_email,add_to_inventory,expense,cancel' + ]; + } + +} diff --git a/app/Http/Requests/TaskStatus/ActionTaskStatusRequest.php b/app/Http/Requests/TaskStatus/ActionTaskStatusRequest.php index a340cd44d281..3df02d2dde86 100644 --- a/app/Http/Requests/TaskStatus/ActionTaskStatusRequest.php +++ b/app/Http/Requests/TaskStatus/ActionTaskStatusRequest.php @@ -24,4 +24,14 @@ class ActionTaskStatusRequest extends Request { return auth()->user()->isAdmin(); } + + public function rules() + { + + return [ + 'ids' => 'required|bail|array', + 'action' => 'in:archive,restore,delete' + ]; + + } } diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index cf94a277657c..20f33d21f733 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -13,6 +13,7 @@ namespace App\Jobs\Util; use App\DataMapper\Analytics\MigrationFailure; use App\DataMapper\CompanySettings; +use App\Exceptions\ClientHostedMigrationException; use App\Exceptions\MigrationValidatorFailed; use App\Exceptions\ProcessingMigrationArchiveFailed; use App\Exceptions\ResourceDependencyMissing; @@ -582,18 +583,42 @@ class Import implements ShouldQueue $validator = null; } + private function testUserDbLocationSanity(array $data): bool + { + + if(Ninja::isSelfHost()) + return true; + + $current_db = config('database.default'); + + $db1_count = User::on('db-ninja-01')->withTrashed()->whereIn('email', array_column($data, 'email'))->count(); + $db2_count = User::on('db-ninja-02')->withTrashed()->whereIn('email', array_column($data, 'email'))->count(); + + MultiDB::setDb($current_db); + + if($db2_count == 0 && $db1_count == 0) + return true; + + if($db1_count >= 1 && $db2_count >= 1) + return false; + + return true; + } + /** * @param array $data * @throws Exception */ private function processUsers(array $data): void { + if(!$this->testUserDbLocationSanity($data)) + throw new ClientHostedMigrationException('You have users that belong to different accounts registered in the system, please contact us to resolve.', 400); + User::unguard(); $rules = [ '*.first_name' => ['string'], '*.last_name' => ['string'], - //'*.email' => ['distinct'], '*.email' => ['distinct', 'email', new ValidUserForCompany()], ]; @@ -749,7 +774,7 @@ class Import implements ShouldQueue Client::reguard(); - Client::with('contacts')->where('company_id', $this->company->id)->cursor()->each(function ($client){ + Client::withTrashed()->with('contacts')->where('company_id', $this->company->id)->cursor()->each(function ($client){ $contact = $client->contacts->sortByDesc('is_primary')->first(); $contact->is_primary = true; diff --git a/app/Jobs/Util/StartMigration.php b/app/Jobs/Util/StartMigration.php index 4d5336bb5027..6d8cb956d2f4 100644 --- a/app/Jobs/Util/StartMigration.php +++ b/app/Jobs/Util/StartMigration.php @@ -11,6 +11,7 @@ namespace App\Jobs\Util; +use App\Exceptions\ClientHostedMigrationException; use App\Exceptions\MigrationValidatorFailed; use App\Exceptions\NonExistingMigrationFile; use App\Exceptions\ProcessingMigrationArchiveFailed; @@ -126,7 +127,7 @@ class StartMigration implements ShouldQueue App::forgetInstance('translator'); $t = app('translator'); $t->replace(Ninja::transformTranslations($this->company->settings)); - } catch (NonExistingMigrationFile | ProcessingMigrationArchiveFailed | ResourceNotAvailableForMigration | MigrationValidatorFailed | ResourceDependencyMissing | \Exception $e) { + } catch (ClientHostedMigrationException | NonExistingMigrationFile | ProcessingMigrationArchiveFailed | ResourceNotAvailableForMigration | MigrationValidatorFailed | ResourceDependencyMissing | \Exception $e) { $this->company->update_products = $update_product_flag; $this->company->save(); diff --git a/app/Mail/MigrationFailed.php b/app/Mail/MigrationFailed.php index 3932108ee4d8..44be3af46b41 100644 --- a/app/Mail/MigrationFailed.php +++ b/app/Mail/MigrationFailed.php @@ -1,7 +1,18 @@ company->getLocale()); + + $special_message = ''; + + if($this->exception instanceof ClientHostedMigrationException) + $special_message = $this->content; return $this ->from(config('mail.from.address'), config('mail.from.name')) ->text('email.migration.failed_text') ->view('email.migration.failed', [ + 'special_message' => $special_message, 'logo' => $this->company->present()->logo(), 'settings' => $this->company->settings, 'is_system' => $this->is_system, diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index b690a6903345..f3550899aee5 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -51,8 +51,35 @@ class TemplateEmail extends Mailable $this->invitation = $invitation; } + /** + * Supports inline attachments for large + * attachments in custom designs + * + * @return string + */ + private function buildLinksForCustomDesign(): string + { + $links = $this->build_email->getAttachmentLinks(); + + if(count($links) == 0) + return ''; + + $link_string = '
Please contact us at contact@invoiceninja.com for more information on this error.
@endif diff --git a/resources/views/email/template/client.blade.php b/resources/views/email/template/client.blade.php index 580e6dd1ee58..b8e3fca9dbbc 100644 --- a/resources/views/email/template/client.blade.php +++ b/resources/views/email/template/client.blade.php @@ -172,6 +172,11 @@{{ ctrans('texts.attachments') }}
+ @endif + @foreach($links as $link) {!! $link ?? '' !!}{{ ctrans('texts.attachments') }}
+ @endif + + @foreach($links as $link) +{!! $link ?? '' !!}
+diff --git a/tests/Feature/PurchaseOrderTest.php b/tests/Feature/PurchaseOrderTest.php index ff5037d6ab2a..323732774d8a 100644 --- a/tests/Feature/PurchaseOrderTest.php +++ b/tests/Feature/PurchaseOrderTest.php @@ -40,6 +40,102 @@ class PurchaseOrderTest extends TestCase $this->makeTestData(); } + public function testPurchaseOrderBulkActions() + { + $i = $this->purchase_order->invitations->first(); + + $data = [ + 'ids' =>[$this->purchase_order->hashed_id], + 'action' => 'archive', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post("/api/v1/purchase_orders/bulk", $data) + ->assertStatus(200); + + $data = [ + 'ids' =>[$this->purchase_order->hashed_id], + 'action' => 'restore', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post("/api/v1/purchase_orders/bulk", $data) + ->assertStatus(200); + + $data = [ + 'ids' =>[$this->purchase_order->hashed_id], + 'action' => 'delete', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post("/api/v1/purchase_orders/bulk", $data) + ->assertStatus(200); + + + $data = [ + 'ids' =>[$this->purchase_order->hashed_id], + 'action' => 'restore', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post("/api/v1/purchase_orders/bulk", $data) + ->assertStatus(200); + + $data = [ + 'ids' =>[$this->purchase_order->hashed_id], + 'action' => 'download', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post("/api/v1/purchase_orders/bulk", $data) + ->assertStatus(200); + + $data = [ + 'ids' =>[], + 'action' => 'archive', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post("/api/v1/purchase_orders/bulk", $data) + ->assertStatus(302); + + $data = [ + 'ids' =>[$this->purchase_order->hashed_id], + 'action' => '', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post("/api/v1/purchase_orders/bulk", $data) + ->assertStatus(302); + + + $data = [ + 'ids' =>[$this->purchase_order->hashed_id], + 'action' => 'molly', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post("/api/v1/purchase_orders/bulk", $data) + ->assertStatus(302); + + } + public function testPurchaseOrderDownloadPDF() { $i = $this->purchase_order->invitations->first();