1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 13:12:50 +01:00

Merge pull request #8201 from turbo124/v5-develop

Patch for client portal bug
This commit is contained in:
David Bomba 2023-01-24 07:25:35 +11:00 committed by GitHub
commit 3a95441c1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 214 additions and 78 deletions

View File

@ -1 +1 @@
5.5.60
5.5.61

View File

@ -448,13 +448,24 @@ class CheckData extends Command
//check contact exists!
if($contact_class::where('company_id', $entity->company_id)->where($client_vendor_key,$entity->{$client_vendor_key})->exists())
{
$invitation = new $entity_obj();
$invitation->company_id = $entity->company_id;
$invitation->user_id = $entity->user_id;
$invitation->{$entity_key} = $entity->id;
$invitation->{$contact_id} = $contact_class::where('company_id', $entity->company_id)->where($client_vendor_key,$entity->{$client_vendor_key})->first()->id;
$invitation->key = Str::random(config('ninja.key_length'));
$this->logMessage("Add invitation for {$entity_key} - {$entity->id}");
$contact = $contact_class::where('company_id', $entity->company_id)->where($client_vendor_key,$entity->{$client_vendor_key})->first();
//double check if an archived invite exists
if($contact && $entity->invitations()->withTrashed()->where($contact_id, $contact->id)->count() != 0) {
$i = $entity->invitations()->withTrashed()->where($contact_id, $contact->id)->first();
$i->restore();
$this->logMessage("Found a valid contact and invitation restoring for {$entity_key} - {$entity->id}");
}
else {
$invitation = new $entity_obj();
$invitation->company_id = $entity->company_id;
$invitation->user_id = $entity->user_id;
$invitation->{$entity_key} = $entity->id;
$invitation->{$contact_id} = $contact->id;
$invitation->key = Str::random(config('ninja.key_length'));
$this->logMessage("Add invitation for {$entity_key} - {$entity->id}");
}
}
else
$this->logMessage("No contact present, so cannot add invitation for {$entity_key} - {$entity->id}");

View File

@ -87,13 +87,15 @@ class ActivityController extends BaseController
{
$default_activities = $request->has('rows') ? $request->input('rows') : 50;
$activities = Activity::orderBy('created_at', 'DESC')->company()
$activities = Activity::orderBy('created_at', 'DESC')
->company()
->take($default_activities);
if ($request->has('react')) {
if(!auth()->user()->isAdmin())
return response()->json(['data' => []], 200);
$activities->where('user_id', auth()->user()->id);
// return response()->json(['data' => []], 200);
$system = ctrans('texts.system');

View File

@ -450,7 +450,7 @@ class BaseController extends Controller
'company.bank_transactions'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if (! $user->isAdmin()) {
if (! $user->hasPermission('view_bank_transaction')) {
$query->where('bank_transactions.user_id', $user->id);
}
},
@ -796,7 +796,7 @@ class BaseController extends Controller
'company.bank_transactions'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at);
if (! $user->isAdmin()) {
if (! $user->hasPermission('bank_transactions')) {
$query->where('bank_transactions.user_id', $user->id);
}
},
@ -861,7 +861,7 @@ class BaseController extends Controller
/**/
// 10-01-2022 need to ensure we snake case properly here to ensure permissions work as expected
// 28-03-2022 this is definitely correct here, do not append _ to the view, it resolved correctly when snake cased
if (auth()->user() && ! auth()->user()->hasPermission('view'.lcfirst(class_basename(Str::snake($this->entity_type))))) {
if (auth()->user() && ! auth()->user()->hasPermission('view_'.Str::snake(class_basename($this->entity_type)))) {
//06-10-2022 - some entities do not have assigned_user_id - this becomes an issue when we have a large company and low permission users
if(in_array($this->entity_type, [User::class])){
$query->where('id', auth()->user()->id);

View File

@ -16,7 +16,7 @@ class Cors
// ALLOW OPTIONS METHOD
$headers = [
'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE',
'Access-Control-Allow-Headers'=> 'X-API-PASSWORD-BASE64,X-API-COMPANY-KEY,X-CLIENT-VERSION,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE',
'Access-Control-Allow-Headers'=> 'X-API-PASSWORD-BASE64,X-API-COMPANY-KEY,X-CLIENT-VERSION,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Disposition,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE',
];
return Response::make('OK', 200, $headers);
@ -26,8 +26,8 @@ class Cors
$response->headers->set('Access-Control-Allow-Origin', '*');
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'X-API-PASSWORD-BASE64,X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE');
$response->headers->set('Access-Control-Expose-Headers', 'X-APP-VERSION,X-MINIMUM-CLIENT-VERSION');
$response->headers->set('Access-Control-Allow-Headers', 'X-API-PASSWORD-BASE64,X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Disposition,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE');
$response->headers->set('Access-Control-Expose-Headers', 'X-APP-VERSION,X-MINIMUM-CLIENT-VERSION,Content-Disposition');
$response->headers->set('X-APP-VERSION', config('ninja.app_version'));
$response->headers->set('X-MINIMUM-CLIENT-VERSION', config('ninja.minimum_client_version'));

View File

@ -42,6 +42,23 @@ class TaskScheduler implements ShouldQueue
*/
public function handle()
{
if (! config('ninja.db.multi_db_enabled')) {
Scheduler::with('company')
->where('is_paused', false)
->where('is_deleted', false)
->whereNotNull('next_run')
->where('next_run', '<=', now())
->cursor()
->each(function ($scheduler) {
$this->doJob($scheduler);
});
return;
}
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);

View File

@ -174,7 +174,7 @@ class BaseModel extends Model
return $this
->withTrashed()
->company()
// ->company()
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
}

View File

@ -240,7 +240,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
{
return $this
->withTrashed()
->company()
// ->company()
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
}

View File

@ -358,18 +358,21 @@ class User extends Authenticatable implements MustVerifyEmail
public function hasPermission($permission) : bool
{
$parts = explode('_', $permission);
$all_permission = '';
$all_permission = false;
if (count($parts) > 1) {
$all_permission = $parts[0].'_all';
}
//empty $all_permissions leads to stripos returning true;
return $this->isOwner() ||
$this->isAdmin() ||
(is_int(stripos($this->token()->cu->permissions, $all_permission))) ||
(is_int(stripos($this->token()->cu->permissions, $permission)));
(stripos($all_permission, $this->token()->cu->permissions) !== false) ||
(stripos($permission, $this->token()->cu->permissions) !== false);
// return $this->isOwner() ||
// $this->isAdmin() ||
// (is_int(stripos($this->token()->cu->permissions, $all_permission))) ||
// (is_int(stripos($this->token()->cu->permissions, $permission)));
}

View File

@ -26,6 +26,6 @@ class BankTransactionPolicy extends EntityPolicy
*/
public function create(User $user) : bool
{
return $user->isAdmin();
return $user->isAdmin() || $user->hasPermission('create_invoice') || $user->hasPermission('create_all');
}
}

View File

@ -47,7 +47,7 @@ class EntityPolicy
public function edit(User $user, $entity) : bool
{
return ($user->isAdmin() && $entity->company_id == $user->companyId())
|| ($user->hasPermission('edit_'.strtolower(\Illuminate\Support\Str::snake(class_basename($entity)))) && $entity->company_id == $user->companyId())
|| ($user->hasPermission('edit_'.\Illuminate\Support\Str::snake(class_basename($entity))) && $entity->company_id == $user->companyId())
|| ($user->hasPermission('edit_all') && $entity->company_id == $user->companyId())
|| ($user->owns($entity) && $entity->company_id == $user->companyId())
|| ($user->assigned($entity) && $entity->company_id == $user->companyId());
@ -64,7 +64,7 @@ class EntityPolicy
public function view(User $user, $entity) : bool
{
return ($user->isAdmin() && $entity->company_id == $user->companyId())
|| ($user->hasPermission('view_'.strtolower(\Illuminate\Support\Str::snake(class_basename($entity)))) && $entity->company_id == $user->companyId())
|| ($user->hasPermission('view_'.\Illuminate\Support\Str::snake(class_basename($entity))) && $entity->company_id == $user->companyId())
|| ($user->hasPermission('view_all') && $entity->company_id == $user->companyId())
|| ($user->owns($entity) && $entity->company_id == $user->companyId())
|| ($user->assigned($entity) && $entity->company_id == $user->companyId());

View File

@ -98,7 +98,7 @@ class SubscriptionService
->save();
//update the invoice and attach to the recurring invoice!!!!!
$invoice = Invoice::find($payment_hash->fee_invoice_id);
$invoice = Invoice::withTrashed()->find($payment_hash->fee_invoice_id);
$invoice->recurring_id = $recurring_invoice->id;
$invoice->is_proforma = false;
$invoice->save();

View File

@ -23,7 +23,7 @@ return [
'allowed_origins_patterns' => [],
'allowed_headers' => ['X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE'],
'allowed_headers' => ['X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Disposition,Content-Type,Range,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE'],
'exposed_headers' => ['X-APP-VERSION,X-MINIMUM-CLIENT-VERSION,X-CSRF-TOKEN,X-XSRF-TOKEN,X-LIVEWIRE'],

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.60',
'app_tag' => '5.5.60',
'app_version' => '5.5.61',
'app_tag' => '5.5.61',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -476,10 +476,10 @@ return new class extends Migration {
$t->string('custom_value4')->nullable();
$t->datetime('next_send_date')->nullable();
$t->string('custom_surcharge1')->nullable();
$t->string('custom_surcharge2')->nullable();
$t->string('custom_surcharge3')->nullable();
$t->string('custom_surcharge4')->nullable();
$t->decimal('custom_surcharge1', 20, 6)->nullable();
$t->decimal('custom_surcharge2', 20, 6)->nullable();
$t->decimal('custom_surcharge3', 20, 6)->nullable();
$t->decimal('custom_surcharge4', 20, 6)->nullable();
$t->boolean('custom_surcharge_tax1')->default(false);
$t->boolean('custom_surcharge_tax2')->default(false);
$t->boolean('custom_surcharge_tax3')->default(false);
@ -554,10 +554,10 @@ return new class extends Migration {
$t->string('custom_value4')->nullable();
$t->datetime('next_send_date')->nullable();
$t->string('custom_surcharge1')->nullable();
$t->string('custom_surcharge2')->nullable();
$t->string('custom_surcharge3')->nullable();
$t->string('custom_surcharge4')->nullable();
$t->decimal('custom_surcharge1', 20, 6)->nullable();
$t->decimal('custom_surcharge2', 20, 6)->nullable();
$t->decimal('custom_surcharge3', 20, 6)->nullable();
$t->decimal('custom_surcharge4', 20, 6)->nullable();
$t->boolean('custom_surcharge_tax1')->default(false);
$t->boolean('custom_surcharge_tax2')->default(false);
$t->boolean('custom_surcharge_tax3')->default(false);
@ -791,10 +791,10 @@ return new class extends Migration {
$t->string('custom_value3')->nullable();
$t->string('custom_value4')->nullable();
$t->string('custom_surcharge1')->nullable();
$t->string('custom_surcharge2')->nullable();
$t->string('custom_surcharge3')->nullable();
$t->string('custom_surcharge4')->nullable();
$t->decimal('custom_surcharge1', 20, 6)->nullable();
$t->decimal('custom_surcharge2', 20, 6)->nullable();
$t->decimal('custom_surcharge3', 20, 6)->nullable();
$t->decimal('custom_surcharge4', 20, 6)->nullable();
$t->boolean('custom_surcharge_tax1')->default(false);
$t->boolean('custom_surcharge_tax2')->default(false);
$t->boolean('custom_surcharge_tax3')->default(false);

View File

@ -59,10 +59,10 @@ return new class extends Migration {
$table->boolean('auto_bill_enabled')->default(0);
$table->unsignedInteger('design_id')->nullable();
$table->boolean('uses_inclusive_taxes')->default(0);
$table->string('custom_surcharge1')->nullable();
$table->string('custom_surcharge2')->nullable();
$table->string('custom_surcharge3')->nullable();
$table->string('custom_surcharge4')->nullable();
$table->decimal('custom_surcharge1', 20, 6)->nullable();
$table->decimal('custom_surcharge2', 20, 6)->nullable();
$table->decimal('custom_surcharge3', 20, 6)->nullable();
$table->decimal('custom_surcharge4', 20, 6)->nullable();
$table->boolean('custom_surcharge_tax1')->default(false);
$table->boolean('custom_surcharge_tax2')->default(false);
$table->boolean('custom_surcharge_tax3')->default(false);

View File

@ -12,33 +12,33 @@ return new class extends Migration {
*/
public function up()
{
Schema::table('invoices', function (Blueprint $table) {
$table->decimal('custom_surcharge1', 20, 6)->change();
$table->decimal('custom_surcharge2', 20, 6)->change();
$table->decimal('custom_surcharge3', 20, 6)->change();
$table->decimal('custom_surcharge4', 20, 6)->change();
});
// Schema::table('invoices', function (Blueprint $table) {
// $table->decimal('custom_surcharge1', 20, 6)->change();
// $table->decimal('custom_surcharge2', 20, 6)->change();
// $table->decimal('custom_surcharge3', 20, 6)->change();
// $table->decimal('custom_surcharge4', 20, 6)->change();
// });
Schema::table('recurring_invoices', function (Blueprint $table) {
$table->decimal('custom_surcharge1', 20, 6)->change();
$table->decimal('custom_surcharge2', 20, 6)->change();
$table->decimal('custom_surcharge3', 20, 6)->change();
$table->decimal('custom_surcharge4', 20, 6)->change();
});
// Schema::table('recurring_invoices', function (Blueprint $table) {
// $table->decimal('custom_surcharge1', 20, 6)->change();
// $table->decimal('custom_surcharge2', 20, 6)->change();
// $table->decimal('custom_surcharge3', 20, 6)->change();
// $table->decimal('custom_surcharge4', 20, 6)->change();
// });
Schema::table('quotes', function (Blueprint $table) {
$table->decimal('custom_surcharge1', 20, 6)->change();
$table->decimal('custom_surcharge2', 20, 6)->change();
$table->decimal('custom_surcharge3', 20, 6)->change();
$table->decimal('custom_surcharge4', 20, 6)->change();
});
// Schema::table('quotes', function (Blueprint $table) {
// $table->decimal('custom_surcharge1', 20, 6)->change();
// $table->decimal('custom_surcharge2', 20, 6)->change();
// $table->decimal('custom_surcharge3', 20, 6)->change();
// $table->decimal('custom_surcharge4', 20, 6)->change();
// });
Schema::table('credits', function (Blueprint $table) {
$table->decimal('custom_surcharge1', 20, 6)->change();
$table->decimal('custom_surcharge2', 20, 6)->change();
$table->decimal('custom_surcharge3', 20, 6)->change();
$table->decimal('custom_surcharge4', 20, 6)->change();
});
// Schema::table('credits', function (Blueprint $table) {
// $table->decimal('custom_surcharge1', 20, 6)->change();
// $table->decimal('custom_surcharge2', 20, 6)->change();
// $table->decimal('custom_surcharge3', 20, 6)->change();
// $table->decimal('custom_surcharge4', 20, 6)->change();
// });
}
/**

View File

@ -2,6 +2,15 @@
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@if(App\Utils\Ninja::isHosted())
<!-- G Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-WMJ5W23');</script>
<!-- End G Tag Manager -->
@endif
<!-- Error: {{ session('error') }} -->
@if (isset($company) && $company->matomo_url && $company->matomo_id)
<script>

View File

@ -18,6 +18,7 @@ use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Invoice;
use App\Services\Bank\ProcessBankRules;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
@ -41,6 +42,19 @@ class BankTransactionRuleTest extends TestCase
$this->withoutExceptionHandling();
}
public function testMatchingWithStripos()
{
$bt_value = strtolower(str_replace(" ", "", 'hello soldier'));
$rule_value = strtolower(str_replace(" ", "", 'solider'));
$rule_length = iconv_strlen($rule_value);
$this->assertFalse(stripos($rule_value, $bt_value) !== false);
$this->assertFalse(stripos($bt_value, $rule_value) !== false);
}
public function testBankRuleBulkActions()
{
$data = [

View File

@ -31,7 +31,7 @@ class ExampleIntegrationTest extends TestCase
public function testExample()
{
$this->markTestIncomplete();
// $this->markTestIncomplete();
$invoice = $this->invoice;
$invitation = $invoice->invitations()->first();
@ -57,10 +57,6 @@ class ExampleIntegrationTest extends TestCase
->design($design)
->build();
// exec('echo "" > storage/logs/laravel.log');
// nlog($maker->getCompiledHTML(true));
$this->assertTrue(true);
$this->assertNotNull($maker->getCompiledHTML(true));
}
}

View File

@ -13,10 +13,12 @@ namespace Tests\Unit;
use App\Factory\CompanyUserFactory;
use App\Models\Account;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\CompanyUser;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\MockAccountData;
@ -77,6 +79,54 @@ class PermissionsTest extends TestCase
}
public function testPermissionResolution()
{
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(Invoice::class)));
$this->assertEquals('view_invoice', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(Client::class)));
$this->assertEquals('view_client', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(RecurringInvoice::class)));
$this->assertEquals('view_recurring_invoice', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\Product::class)));
$this->assertEquals('view_product', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\Payment::class)));
$this->assertEquals('view_payment', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\Quote::class)));
$this->assertEquals('view_quote', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\Credit::class)));
$this->assertEquals('view_credit', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\Project::class)));
$this->assertEquals('view_project', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\Task::class)));
$this->assertEquals('view_task', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\Vendor::class)));
$this->assertEquals('view_vendor', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\PurchaseOrder::class)));
$this->assertEquals('view_purchase_order', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\Expense::class)));
$this->assertEquals('view_expense', $class);
$class = 'view'.lcfirst(class_basename(\Illuminate\Support\Str::snake(App\Models\BankTransaction::class)));
$this->assertEquals('view_bank_transaction', $class);
$this->assertEquals('invoice', \Illuminate\Support\Str::snake(class_basename(Invoice::class)));
$this->assertEquals('recurring_invoice', \Illuminate\Support\Str::snake(class_basename(RecurringInvoice::class)));
}
public function testExactPermissions()
{
@ -109,6 +159,40 @@ class PermissionsTest extends TestCase
}
public function testReturnTypesOfStripos()
{
$this->assertEquals(0, stripos("view_client", ''));
$all_permission = '[]';
$this->assertFalse(stripos($all_permission, "view_client") !== false);
$this->assertTrue(stripos($all_permission, "view_client") == 0);
$this->assertFalse(is_int(stripos($all_permission, "view_client")));
$all_permission = ' ';
$this->assertFalse(stripos($all_permission, "view_client") !== false);
$this->assertFalse(is_int(stripos($all_permission, "view_client")));
$all_permission = "";//problems are empty strings
$this->assertTrue(empty($all_permission));
$this->assertFalse( stripos($all_permission, "view_client") !== false);
$this->assertFalse( is_int(stripos($all_permission, "view_client")));
$all_permission = 'view';//will always pass currently
$this->assertFalse( stripos($all_permission, "view_client") !== false);
$this->assertFalse(is_int(stripos($all_permission, "view_client")));
$all_permission = "view_client";
$this->assertTrue(stripos($all_permission, "view_client") !== false);
$this->assertTrue(is_int(stripos($all_permission, "view_client")) !== false);
$this->assertTrue(is_int(stripos($all_permission, "view_client")));
}
public function testViewClientPermission()
{
@ -116,8 +200,8 @@ class PermissionsTest extends TestCase
$low_cu->permissions = '["view_client"]';
$low_cu->save();
//this is aberrant
$this->assertTrue($this->user->hasPermission("viewclient"));
// this is aberrant
$this->assertFalse($this->user->hasPermission("view____client"));
}