From 4eb7b7b0b6735cca91ade2b57d61bcb728e1d349 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 25 May 2025 09:51:52 +1000 Subject: [PATCH 01/15] Updated lock --- composer.json | 4 ++-- composer.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 17a06e6b32..0a99725f18 100644 --- a/composer.json +++ b/composer.json @@ -226,8 +226,8 @@ "url": "https://github.com/turbo124/snappdf" }, { - "type": "path", - "url": "../admin-api" + "type": "vcs", + "url": "https://github.com/invoiceninja/admin-api" } ], "minimum-stability": "dev", diff --git a/composer.lock b/composer.lock index a2839e0932..160b4e9b62 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4361272676d998d08fb1926198065b39", + "content-hash": "4c133d6a99108c7c4e779dfdab4457e3", "packages": [ { "name": "adrienrn/php-mimetyper", From 8e30fd730533ae3c91c05ccb2f1c7e61caad84ee Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 25 May 2025 11:55:52 +1000 Subject: [PATCH 02/15] Updated elastic searchable trait --- app/Models/Client.php | 2 +- app/Models/ClientContact.php | 2 +- app/Models/Credit.php | 2 +- app/Models/Expense.php | 2 +- app/Models/PurchaseOrder.php | 2 +- app/Models/Quote.php | 2 +- app/Models/RecurringInvoice.php | 2 +- app/Models/Vendor.php | 2 +- app/Models/VendorContact.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/Models/Client.php b/app/Models/Client.php index ad52770086..32c1627541 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -12,7 +12,7 @@ namespace App\Models; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use App\DataMapper\ClientSync; use App\Utils\Traits\AppSetup; use App\Utils\Traits\MakesHash; diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index 73762a6c22..8ca2551742 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -14,7 +14,7 @@ namespace App\Models; use App\Utils\Ninja; use Illuminate\Support\Str; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use App\Jobs\Mail\NinjaMailer; use App\Utils\Traits\AppSetup; use App\Utils\Traits\MakesHash; diff --git a/app/Models/Credit.php b/app/Models/Credit.php index f3580db888..67a32de32b 100644 --- a/app/Models/Credit.php +++ b/app/Models/Credit.php @@ -14,7 +14,7 @@ namespace App\Models; use App\Utils\Ninja; use App\Utils\Number; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use Illuminate\Support\Carbon; use App\Utils\Traits\MakesHash; use App\Helpers\Invoice\InvoiceSum; diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 1002492dbb..6f27da21d9 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -13,7 +13,7 @@ namespace App\Models; use App\Utils\Number; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use Illuminate\Support\Facades\App; use Illuminate\Database\Eloquent\SoftDeletes; diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index bd4a105f3c..a93b1cf4f0 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -14,7 +14,7 @@ namespace App\Models; use App\Utils\Ninja; use App\Utils\Number; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use Illuminate\Support\Carbon; use App\Helpers\Invoice\InvoiceSum; use Illuminate\Support\Facades\App; diff --git a/app/Models/Quote.php b/app/Models/Quote.php index 36295a4daf..992456593b 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -15,7 +15,7 @@ namespace App\Models; use App\Utils\Ninja; use App\Utils\Number; use App\DataMapper\QuoteSync; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use Illuminate\Support\Carbon; use App\Utils\Traits\MakesHash; use App\Helpers\Invoice\InvoiceSum; diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index d6ee6df2d2..d4dec50630 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -13,7 +13,7 @@ namespace App\Models; use App\Utils\Number; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use Illuminate\Support\Carbon; use App\Utils\Traits\MakesHash; use App\Helpers\Invoice\InvoiceSum; diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 890e10f724..99ef857753 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -12,7 +12,7 @@ namespace App\Models; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use App\Utils\Traits\AppSetup; use App\DataMapper\CompanySettings; use Illuminate\Support\Facades\App; diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php index 81b0532637..a7e9d93149 100644 --- a/app/Models/VendorContact.php +++ b/app/Models/VendorContact.php @@ -12,7 +12,7 @@ namespace App\Models; -use Laravel\Scout\Searchable; +use Elastic\ScoutDriverPlus\Searchable; use App\Utils\Traits\MakesHash; use Illuminate\Support\Facades\Cache; use Illuminate\Database\Eloquent\Model; From dd48544dfc3961d39aa17bab0d9bc89945ab415e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 25 May 2025 12:02:43 +1000 Subject: [PATCH 03/15] catch when paypal fails --- app/PaymentDrivers/PayPalPPCPPaymentDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php index fd86f9f997..2468c6ca14 100644 --- a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php +++ b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php @@ -167,7 +167,7 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver nlog($response); } - if (isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) { + if (is_array($response) && isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) { return $this->createNinjaPayment($request, $response); From 9abeb926ddbb670f8dc17c5590866f8ef8837a30 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 25 May 2025 18:47:32 +1000 Subject: [PATCH 04/15] Minor fixes --- app/Models/Account.php | 2 +- app/PaymentDrivers/Authorize/AuthorizeCreditCard.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Models/Account.php b/app/Models/Account.php index bfbabd13ea..eb7d11cc02 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -310,7 +310,7 @@ class Account extends BaseModel } // 09-03-2023 - winds forward expiry checks to ensure we don't cut off users prior to billing cycle being commenced - if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()->subHours(12))) { + if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()->subHours(23))) { return false; } diff --git a/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php b/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php index 23580a5415..8cb6eac666 100644 --- a/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php +++ b/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php @@ -233,7 +233,7 @@ class AuthorizeCreditCard implements LivewireMethodInterface private function processSuccessfulResponse($data, $request) { $payment_hash = PaymentHash::where('hash', $request->input('payment_hash'))->firstOrFail(); - $payment = $this->storePayment($payment_hash, $data); + $payment = $this->storePayment($payment_hash, $data, \App\Models\GatewayType::CREDIT_CARD); $vars = [ 'invoices' => $payment_hash->invoices(), From 704fe8468dd95107e6350ce021628628b5e2fc95 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 26 May 2025 08:32:43 +1000 Subject: [PATCH 05/15] Fixes for auth.net --- app/PaymentDrivers/Authorize/AuthorizeCreditCard.php | 1 + app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php b/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php index 8cb6eac666..548ced05e1 100644 --- a/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php +++ b/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php @@ -80,6 +80,7 @@ class AuthorizeCreditCard implements LivewireMethodInterface if ($request->has('store_card') && $request->input('store_card') === true) { $authorise_payment_method = new AuthorizePaymentMethod($this->authorize); + $authorise_payment_method->setPaymentMethodId(\App\Models\GatewayType::CREDIT_CARD); $payment_profile = $authorise_payment_method->addPaymentMethodToClient($gateway_customer_reference, $data); $payment_profile_id = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId(); diff --git a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php index f585abb587..4695295ab2 100644 --- a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php +++ b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php @@ -39,6 +39,12 @@ class AuthorizePaymentMethod private $payment_method_id; + public function setPaymentMethodId($payment_method_id) + { + $this->payment_method_id = $payment_method_id; + return $this; + } + public function __construct(AuthorizePaymentDriver $authorize) { $this->authorize = $authorize; From c8b9c3682b14a66f0b3cce17e9142f523cbbf8c6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 26 May 2025 12:30:44 +1000 Subject: [PATCH 06/15] Refactor for login --- app/Console/Commands/S3Cleanup.php | 3 +- app/Http/Controllers/Auth/LoginController.php | 143 ++++++++++-------- app/Libraries/MultiDB.php | 4 +- 3 files changed, 81 insertions(+), 69 deletions(-) diff --git a/app/Console/Commands/S3Cleanup.php b/app/Console/Commands/S3Cleanup.php index 6578b96aa9..93140682f0 100644 --- a/app/Console/Commands/S3Cleanup.php +++ b/app/Console/Commands/S3Cleanup.php @@ -57,8 +57,9 @@ class S3Cleanup extends Command $c1 = Company::on('db-ninja-01')->pluck('company_key'); $c2 = Company::on('db-ninja-02')->pluck('company_key'); + $c3 = Company::on('db-ninja-03')->pluck('company_key'); - $merged = $c1->merge($c2)->toArray(); + $merged = $c1->merge($c2)->merge($c3)->toArray(); $directories = Storage::disk(config('filesystems.default'))->directories(); diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index e613e6e4d2..809a9def78 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -39,6 +39,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Response; use Laravel\Socialite\Facades\Socialite; use Microsoft\Graph\Model; @@ -107,7 +108,13 @@ class LoginController extends BaseController ->header('X-Api-Version', config('ninja.minimum_client_version')); } - if ($this->attemptLogin($request)) { + // Direct user query based on email and password verification + $user = MultiDB::hasUser(['email' => $request->email]); + + if ($user && \Illuminate\Support\Facades\Hash::check(trim($request->password), $user->password)) { + // Manually log the user in + Auth::login($user, false); + LightLogs::create(new LoginSuccess()) ->increment() ->batch(); @@ -115,9 +122,6 @@ class LoginController extends BaseController LightLogs::create(new LoginMeta($request->email, $request->ip, 'success')) ->batch(); - /** @var \App\Models\User $user */ - $user = $this->guard()->user(); - //2FA if ($user->google_2fa_secret && $request->has('one_time_password')) { $google2fa = new Google2FA(); @@ -144,7 +148,7 @@ class LoginController extends BaseController } /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser(); + $cu = $this->hydrateCompanyUser($user); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); @@ -280,10 +284,10 @@ class LoginController extends BaseController return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); } - Auth::login($existing_user, true); + Auth::login($existing_user, false); /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser(); + $cu = $this->hydrateCompanyUser($existing_user); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); @@ -296,34 +300,34 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } //If this is a result user/email combo - lets add their OAuth details details - if ($existing_login_user = MultiDB::hasUser(['email' => $user->email])) { - if (!$existing_login_user->account) { - return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); - } + // if ($existing_login_user = MultiDB::hasUser(['email' => $user->email])) { + // if (!$existing_login_user->account) { + // return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); + // } - Auth::login($existing_login_user, true); - /** @var \App\Models\User $user */ + // Auth::login($existing_login_user, true); + // /** @var \App\Models\User $user */ - $user = auth()->user(); + // $user = auth()->user(); - $user->update([ - 'oauth_user_id' => $user->id, - 'oauth_provider_id' => $provider, - ]); + // $user->update([ + // 'oauth_user_id' => $user->id, + // 'oauth_provider_id' => $provider, + // ]); - /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser(); + // /** @var \App\Models\CompanyUser $cu */ + // $cu = $this->hydrateCompanyUser(); - if ($cu->count() == 0) { - return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - } + // if ($cu->count() == 0) { + // return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); + // } - if (Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterprisePaidClient()) { - return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); - } + // if (Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterprisePaidClient()) { + // return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); + // } - return $this->timeConstrainedResponse($cu); - } + // return $this->timeConstrainedResponse($cu); + // } // nlog("socialite"); // nlog($user); @@ -352,16 +356,21 @@ class LoginController extends BaseController $account = (new CreateAccount($new_account, request()->getClientIp()))->handle(); - Auth::login($account->default_company->owner(), true); + $user = $account->default_company->owner(); + // Auth::login($user, false); - /** @var \App\Models\User $user */ - $user = auth()->user(); + auth()->login($user, false); + auth()->user()->setCompany($account->default_company); + + + // /** @var \App\Models\User $user */ + // $user = auth()->user(); $user->email_verified_at = now(); $user->save(); /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser(); + $cu = $this->hydrateCompanyUser($user); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); @@ -374,12 +383,14 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } - private function hydrateCompanyUser(): Builder + private function hydrateCompanyUser($user): Builder { - /** @var \App\Models\User $user */ - $user = auth()->user(); + // /** @var \App\Models\User $user */ + // $user = auth()->user(); + MultiDB::hasUser(['email' => $user->email]); + /** @var Builder $cu */ $cu = CompanyUser::query()->where('user_id', $user->id); @@ -499,7 +510,7 @@ class LoginController extends BaseController Auth::login($existing_user, true); /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser(); + $cu = $this->hydrateCompanyUser($existing_user); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); @@ -512,30 +523,30 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } - private function existingLoginUser($oauth_user_id, $provider) - { + // private function existingLoginUser($oauth_user_id, $provider) + // { - /** @var \App\Models\User $user */ - $user = auth()->user(); + // /** @var \App\Models\User $user */ + // $user = auth()->user(); - $user->update([ - 'oauth_user_id' => $oauth_user_id, - 'oauth_provider_id' => $provider, - ]); + // $user->update([ + // 'oauth_user_id' => $oauth_user_id, + // 'oauth_provider_id' => $provider, + // ]); - /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser(); + // /** @var \App\Models\CompanyUser $cu */ + // $cu = $this->hydrateCompanyUser($user); //should never hit - if ($cu->count() == 0) { - return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - } + // if ($cu->count() == 0) { + // return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); + // } - if (Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterprisePaidClient()) { - return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); - } + // if (Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterprisePaidClient()) { + // return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); + // } - return $this->timeConstrainedResponse($cu); - } + // return $this->timeConstrainedResponse($cu); + // } private function handleGoogleOauth() { @@ -584,15 +595,15 @@ class LoginController extends BaseController if ($user) { //check the user doesn't already exist in some form - if ($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user), 'oauth_provider_id' => 'google'])) { - if (!$existing_login_user->account) { - return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); - } + // if ($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user), 'oauth_provider_id' => 'google'])) { + // if (!$existing_login_user->account) { + // return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); + // } - Auth::login($existing_login_user, true); + // Auth::login($existing_login_user, true); - return $this->existingLoginUser($google->harvestSubField($user), 'google'); - } + // return $this->existingLoginUser($google->harvestSubField($user), 'google'); + // } if (request()->has('create') && request()->input('create') == 'true') { //user not found anywhere - lets sign them up. @@ -628,16 +639,16 @@ class LoginController extends BaseController return $account; } - Auth::login($account->default_company->owner(), true); - - /** @var \App\Models\User $user */ - $user = auth()->user(); + $user = $account->default_company->owner(); + // Auth::login($user, true); + auth()->login($user, false); + auth()->user()->setCompany($account->default_company); $user->email_verified_at = now(); $user->save(); /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser(); + $cu = $this->hydrateCompanyUser($user); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); diff --git a/app/Libraries/MultiDB.php b/app/Libraries/MultiDB.php index ebbbbcec81..02f92f9a18 100644 --- a/app/Libraries/MultiDB.php +++ b/app/Libraries/MultiDB.php @@ -208,7 +208,7 @@ class MultiDB foreach (self::$dbs as $db) { self::setDB($db); - if ($user = User::where($data)->withTrashed()->first()) { + if ($user = User::on($db)->where($data)->withTrashed()->first()) { return $user; } } @@ -653,7 +653,7 @@ class MultiDB foreach (self::$dbs as $db) { self::setDB($db); - if ($exists = Account::where('account_sms_verification_number', $phone)->where('account_sms_verified', true)->exists()) { + if ($exists = Account::on($db)->where('account_sms_verification_number', $phone)->where('account_sms_verified', true)->exists()) { self::setDb($current_db); return true; } From e2988a8c711926ca7469d01523e857137c15b708 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 26 May 2025 16:26:03 +1000 Subject: [PATCH 07/15] Refactor for loading company user relation --- app/Filters/InvoiceFilters.php | 14 ++ app/Http/Controllers/Auth/LoginController.php | 82 +++++---- app/Http/Middleware/TokenAuth.php | 2 + app/Models/CompanyToken.php | 8 +- app/Models/CreditInvitation.php | 1 + app/Models/Invoice.php | 2 +- app/Models/InvoiceInvitation.php | 1 + app/Models/PurchaseOrderInvitation.php | 1 + app/Models/QuoteInvitation.php | 1 + app/Models/User.php | 160 +++++++++++++----- app/Providers/AppServiceProvider.php | 10 +- app/Repositories/ActivityRepository.php | 2 +- bootstrap/app.php | 10 ++ phpstan.neon | 1 + 14 files changed, 203 insertions(+), 92 deletions(-) diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 4165544d43..465a8efb93 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -296,6 +296,20 @@ class InvoiceFilters extends QueryFilters return $this->builder->orderByRaw("REGEXP_REPLACE(invoices.number,'[^0-9]+','')+0 " . $dir); } + if ($sort_col[0] == 'status_id') { + // Special handling for status_id==2 (STATUS_SENT) with sub-statuses + return $this->builder->orderByRaw(" + CASE + WHEN status_id != 2 THEN status_id + WHEN status_id = 2 AND (due_date IS NOT NULL AND (due_date < NOW() OR partial_due_date < NOW())) THEN 2.9 + WHEN status_id = 2 AND last_viewed IS NOT NULL THEN 2.5 + WHEN status_id = 2 THEN 2.2 + ELSE status_id + END " . $dir); + + } + + return $this->builder->orderBy("{$this->builder->getQuery()->from}.".$sort_col[0], $dir); } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 809a9def78..473befb5d4 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -73,20 +73,6 @@ class LoginController extends BaseController parent::__construct(); } - /** - * Once the user is authenticated, we need to set - * the default company into a session variable. - * - * @param Request $request - * @param User $user - * @return void - * @deprecated .1 API ONLY we don't need to set any session variables - */ - public function authenticated(Request $request, User $user): void - { - //$this->setCurrentCompanyId($user->companies()->first()->account->default_company_id); - } - /** * Login via API. * @@ -97,8 +83,10 @@ class LoginController extends BaseController { $this->forced_includes = ['company_users']; + /** Checks the required fields for auth are present */ $this->validateLogin($request); + /** Native laravel login throttling */ if ($this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); @@ -112,17 +100,17 @@ class LoginController extends BaseController $user = MultiDB::hasUser(['email' => $request->email]); if ($user && \Illuminate\Support\Facades\Hash::check(trim($request->password), $user->password)) { - // Manually log the user in + //Authenticate for this request only. Auth::login($user, false); LightLogs::create(new LoginSuccess()) ->increment() ->batch(); - LightLogs::create(new LoginMeta($request->email, $request->ip, 'success')) + LightLogs::create(new LoginMeta($request->email, $request->ip(), 'success')) ->batch(); - //2FA + // Process2FA on this request if the parameters are present. if ($user->google_2fa_secret && $request->has('one_time_password')) { $google2fa = new Google2FA(); @@ -150,6 +138,8 @@ class LoginController extends BaseController /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser($user); + nlog("LOGIN:: ".$request->email." {$user->account_id}"); + if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); } @@ -167,7 +157,7 @@ class LoginController extends BaseController ->increment() ->batch(); - LightLogs::create(new LoginMeta($request->email, $request->ip, 'failure')) + LightLogs::create(new LoginMeta($request->email, $request->ip(), 'failure')) ->batch(); $this->incrementLoginAttempts($request); @@ -189,11 +179,11 @@ class LoginController extends BaseController { $truth = app()->make(TruthSource::class); - if ($truth->getCompanyToken()) { - $company_token = $truth->getCompanyToken(); - } else { + // if ($truth->getCompanyToken()) { + // $company_token = $truth->getCompanyToken(); + // } else { $company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first(); - } + // } $cu = CompanyUser::query() ->where('user_id', $company_token->user_id); @@ -252,12 +242,27 @@ class LoginController extends BaseController ->header('X-App-Version', config('ninja.app_version')) ->header('X-Api-Version', config('ninja.minimum_client_version')); } - + + /** + * getSocialiteUser + * + * Returns the socialite user if successful + * @param string $provider + * @param string $token + */ private function getSocialiteUser(string $provider, string $token) { return Socialite::driver($provider)->userFromToken($token); } - + + /** + * handleSocialiteLogin + * + * Handles authentication for Apple OAuth only! + * + * @param string $provider + * @param string $token + */ private function handleSocialiteLogin($provider, $token) { $user = $this->getSocialiteUser($provider, $token); @@ -271,7 +276,13 @@ class LoginController extends BaseController ->header('X-App-Version', config('ninja.app_version')) ->header('X-Api-Version', config('ninja.minimum_client_version')); } - + + /** + * loginOrCreateFromSocialite + * + * @param mixed $user + * @param string $provider + */ private function loginOrCreateFromSocialite($user, $provider) { $query = [ @@ -356,16 +367,11 @@ class LoginController extends BaseController $account = (new CreateAccount($new_account, request()->getClientIp()))->handle(); - $user = $account->default_company->owner(); - // Auth::login($user, false); + $user = $account->users()->first(); auth()->login($user, false); auth()->user()->setCompany($account->default_company); - - // /** @var \App\Models\User $user */ - // $user = auth()->user(); - $user->email_verified_at = now(); $user->save(); @@ -382,7 +388,15 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } - + + /** + * hydrateCompanyUser + * + * Hydrates the company user for the response + * + * @param User $user + * @return Builder + */ private function hydrateCompanyUser($user): Builder { @@ -414,8 +428,6 @@ class LoginController extends BaseController $truth->setCompany($set_company); //21-03-2024 - - $cu->each(function ($cu) { /** @var \App\Models\CompanyUser $cu */ if (CompanyToken::query()->where('company_id', $cu->company_id)->where('user_id', $cu->user_id)->where('is_system', true)->doesntExist()) { @@ -507,7 +519,7 @@ class LoginController extends BaseController */ private function existingOauthUser($existing_user) { - Auth::login($existing_user, true); + Auth::login($existing_user, false); /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser($existing_user); diff --git a/app/Http/Middleware/TokenAuth.php b/app/Http/Middleware/TokenAuth.php index c0e5770c84..c1535d030e 100644 --- a/app/Http/Middleware/TokenAuth.php +++ b/app/Http/Middleware/TokenAuth.php @@ -39,6 +39,7 @@ class TokenAuth 'user.account', 'company', 'account', + 'cu', ])->where('token', $request->header('X-API-TOKEN'))->first())) { } else { return response()->json(['message' => 'Invalid token'], 403); @@ -70,6 +71,7 @@ class TokenAuth | us to decouple a $user and their attached companies completely. | */ + $truth = app()->make(TruthSource::class); $truth->setCompanyUser($company_token->cu); diff --git a/app/Models/CompanyToken.php b/app/Models/CompanyToken.php index d87bdc63b5..213dcc673f 100644 --- a/app/Models/CompanyToken.php +++ b/app/Models/CompanyToken.php @@ -84,9 +84,7 @@ class CompanyToken extends BaseModel public function company_user(): \Illuminate\Database\Eloquent\Relations\HasOne { - return $this->hasOne(CompanyUser::class, 'user_id', 'user_id') - ->where('company_id', $this->company_id) - ->where('user_id', $this->user_id); + return $this->hasOne(CompanyUser::class, ['user_id', 'company_id'], ['user_id', 'company_id']); } /** @@ -94,8 +92,6 @@ class CompanyToken extends BaseModel */ public function cu() { - return $this->hasOne(CompanyUser::class, 'user_id', 'user_id') - ->where('company_id', $this->company_id) - ->where('user_id', $this->user_id); + return $this->hasOne(CompanyUser::class, ['user_id', 'company_id'], ['user_id', 'company_id']); } } diff --git a/app/Models/CreditInvitation.php b/app/Models/CreditInvitation.php index f2f2935502..18b59a8d32 100644 --- a/app/Models/CreditInvitation.php +++ b/app/Models/CreditInvitation.php @@ -153,6 +153,7 @@ class CreditInvitation extends BaseModel public function markViewed() { $this->viewed_date = Carbon::now(); + $this->credit->last_viewed = Carbon::now(); $this->save(); } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 283768f99d..ca43a26a4c 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -271,7 +271,7 @@ class Invoice extends BaseModel 'custom_value4' => (string)$this->custom_value4, 'company_key' => $this->company->company_key, 'po_number' => (string)$this->po_number, - 'line_items' => $this->line_items, + // 'line_items' => $this->line_items, ]; } diff --git a/app/Models/InvoiceInvitation.php b/app/Models/InvoiceInvitation.php index f1628148e2..4b60376efe 100644 --- a/app/Models/InvoiceInvitation.php +++ b/app/Models/InvoiceInvitation.php @@ -148,6 +148,7 @@ class InvoiceInvitation extends BaseModel public function markViewed(): void { $this->viewed_date = Carbon::now(); + $this->invoice->last_viewed = Carbon::now(); $this->save(); } diff --git a/app/Models/PurchaseOrderInvitation.php b/app/Models/PurchaseOrderInvitation.php index 9ae8ecfdd4..bf822f3013 100644 --- a/app/Models/PurchaseOrderInvitation.php +++ b/app/Models/PurchaseOrderInvitation.php @@ -139,6 +139,7 @@ class PurchaseOrderInvitation extends BaseModel public function markViewed(): void { $this->viewed_date = Carbon::now(); + $this->purchase_order->last_viewed = Carbon::now(); $this->save(); } diff --git a/app/Models/QuoteInvitation.php b/app/Models/QuoteInvitation.php index 3a56c04281..870394793e 100644 --- a/app/Models/QuoteInvitation.php +++ b/app/Models/QuoteInvitation.php @@ -139,6 +139,7 @@ class QuoteInvitation extends BaseModel public function markViewed() { $this->viewed_date = Carbon::now(); + $this->quote->last_viewed = Carbon::now(); $this->save(); } diff --git a/app/Models/User.php b/app/Models/User.php index 33eddc3737..bada672a44 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -228,18 +228,27 @@ class User extends Authenticatable implements MustVerifyEmail public function token() { - $truth = app()->make(TruthSource::class); - - if ($truth->getCompanyToken()) { - return $truth->getCompanyToken(); + // Try to get from TruthSource if container is ready + try { + $truth = app()->make(TruthSource::class); + if ($truth->getCompanyToken()) { + return $truth->getCompanyToken(); + } + } catch (\Exception $e) { + // TruthSource not available yet, continue with fallback } - // if (request()->header('X-API-TOKEN')) { + // Fallback to API token lookup if (request()->header('X-API-TOKEN')) { - return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); + $token = CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); + if ($token) { + return $token; + } } - return $this->tokens()->first(); + // Final fallback to user's first token + $token = $this->tokens()->with(['cu'])->first(); + return $token; } /** @@ -270,16 +279,27 @@ class User extends Authenticatable implements MustVerifyEmail */ public function getCompany(): ?Company { - $truth = app()->make(TruthSource::class); - // @phpstan-ignore-next-line if ($this->company) { return $this->company; - } elseif ($truth->getCompany()) { - return $truth->getCompany(); - } elseif (request()->header('X-API-TOKEN')) { + } + + // Try to get from TruthSource if container is ready + try { + $truth = app()->make(TruthSource::class); + if ($truth->getCompany()) { + return $truth->getCompany(); + } + } catch (\Exception $e) { + // TruthSource not available yet, continue with fallback + } + + // Fallback to API token lookup + if (request()->header('X-API-TOKEN')) { $company_token = CompanyToken::with('company')->where('token', request()->header('X-API-TOKEN'))->first(); - return $company_token->company; + if ($company_token) { + return $company_token->company; + } } throw new \Exception('No Company Found'); @@ -305,31 +325,39 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasMany(CompanyUser::class)->withTrashed(); } - public function co_user() - { - $truth = app()->make(TruthSource::class); + // public function co_user() + // { + // $truth = app()->make(TruthSource::class); - if ($truth->getCompanyUser()) { - return $truth->getCompanyUser(); - } + // if ($truth->getCompanyUser()) { + // return $truth->getCompanyUser(); + // } - return $this->token()->cu; - } + // return $this->token()->cu; + // } public function company_user() { - if ($this->companyId()) { - return $this->belongsTo(CompanyUser::class)->where('company_id', $this->companyId())->withTrashed(); + try { + if ($this->companyId()) { + return $this->belongsTo(CompanyUser::class)->where('company_id', $this->companyId())->withTrashed(); + } + } catch (\Exception $e) { + // companyId() failed, continue with fallback } - $truth = app()->make(TruthSource::class); - - if ($truth->getCompanyUser()) { - return $truth->getCompanyUser(); + // Try to get from TruthSource if container is ready + try { + $truth = app()->make(TruthSource::class); + if ($truth->getCompanyUser()) { + return $truth->getCompanyUser(); + } + } catch (\Exception $e) { + // TruthSource not available yet, continue with fallback } - return $this->token()->cu; - + $token = $this->token(); + return $token ? $token->cu : null; } /** @@ -354,8 +382,12 @@ class User extends Authenticatable implements MustVerifyEmail */ public function permissions() { - return $this->token()->cu->permissions; - + $token = $this->token(); + if (!$token || !$token->cu) { + return ''; + } + + return $token->cu->permissions; } /** @@ -365,8 +397,12 @@ class User extends Authenticatable implements MustVerifyEmail */ public function settings() { - return json_decode($this->token()->cu->settings); - + $token = $this->token(); + if (!$token || !$token->cu) { + return new \stdClass(); + } + + return json_decode($token->cu->settings); } /** @@ -376,13 +412,22 @@ class User extends Authenticatable implements MustVerifyEmail */ public function isAdmin(): bool { - return $this->token()->cu->is_admin; - + $token = $this->token(); + if (!$token || !$token->cu) { + return false; + } + + return $token->cu->is_admin; } public function isOwner(): bool { - return $this->token()->cu->is_owner; + $token = $this->token(); + if (!$token || !$token->cu) { + return false; + } + + return $token->cu->is_owner; } public function hasOwnerFlag(): bool @@ -396,7 +441,12 @@ class User extends Authenticatable implements MustVerifyEmail */ public function isSuperUser(): bool { - return $this->token()->cu->is_owner || $this->token()->cu->is_admin; + $token = $this->token(); + if (!$token || !$token->cu) { + return false; + } + + return $token->cu->is_owner || $token->cu->is_admin; } /** @@ -466,11 +516,16 @@ class User extends Authenticatable implements MustVerifyEmail } } + $token = $this->token(); + if (!$token || !$token->cu) { + return false; + } + return $this->isSuperUser() || - (stripos($this->token()->cu->permissions, $permission) !== false) || - (stripos($this->token()->cu->permissions, $all_permission) !== false) || - (stripos($this->token()->cu->permissions, $edit_all) !== false) || - (stripos($this->token()->cu->permissions, $edit_entity) !== false); + (stripos($token->cu->permissions, $permission) !== false) || + (stripos($token->cu->permissions, $all_permission) !== false) || + (stripos($token->cu->permissions, $edit_all) !== false) || + (stripos($token->cu->permissions, $edit_entity) !== false); } /** @@ -492,8 +547,13 @@ class User extends Authenticatable implements MustVerifyEmail $all_permission = $parts[0].'_all'; } - return (stripos($this->token()->cu->permissions, $all_permission) !== false) || - (stripos($this->token()->cu->permissions, $permission) !== false); + $token = $this->token(); + if (!$token || !$token->cu) { + return false; + } + + return (stripos($token->cu->permissions, $all_permission) !== false) || + (stripos($token->cu->permissions, $permission) !== false); } /** @@ -529,7 +589,12 @@ class User extends Authenticatable implements MustVerifyEmail */ public function hasExactPermission(string $permission = '___'): bool { - return (stripos($this->token()->cu->permissions ?? '', $permission) !== false); + $token = $this->token(); + if (!$token || !$token->cu) { + return false; + } + + return (stripos($token->cu->permissions ?? '', $permission) !== false); } @@ -617,9 +682,12 @@ class User extends Authenticatable implements MustVerifyEmail public function routeNotificationForSlack($notification) { - if ($this->token()->cu->slack_webhook_url) { - return $this->token()->cu->slack_webhook_url; + $token = $this->token(); + if ($token && $token->cu && $token->cu->slack_webhook_url) { + return $token->cu->slack_webhook_url; } + + return null; } public function routeNotificationForMail($notification) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ace5c61d90..c5da5354db 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -83,12 +83,15 @@ class AppServiceProvider extends ServiceProvider /* Ensure we don't have stale state in jobs */ Queue::before(function (JobProcessing $event) { - App::forgetInstance('truthsource'); + App::forgetInstance(TruthSource::class); }); /* Always init a new instance everytime the container boots */ - app()->instance(TruthSource::class, new TruthSource()); - + + // app()->instance(TruthSource::class, new TruthSource()); + + + /* Extension for custom mailers */ Mail::extend('gmail', function () { @@ -155,5 +158,6 @@ class AppServiceProvider extends ServiceProvider public function register(): void { + } } diff --git a/app/Repositories/ActivityRepository.php b/app/Repositories/ActivityRepository.php index d956b39595..f35904a725 100644 --- a/app/Repositories/ActivityRepository.php +++ b/app/Repositories/ActivityRepository.php @@ -119,7 +119,7 @@ class ActivityRepository extends BaseRepository public function getTokenId(array $event_vars) { - if ($event_vars['token']) { + if (isset($event_vars['token']) &&$event_vars['token']) { /** @var \App\Models\CompanyToken $company_token **/ $company_token = CompanyToken::query()->where('token', $event_vars['token'])->first(); diff --git a/bootstrap/app.php b/bootstrap/app.php index 037e17df03..bf7543614f 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -41,6 +41,16 @@ $app->singleton( App\Exceptions\Handler::class ); +/* +|-------------------------------------------------------------------------- +| Early TruthSource Binding +|-------------------------------------------------------------------------- +| Bind TruthSource early to prevent issues with early access +*/ +$app->bind(\App\Utils\TruthSource::class, function () { + return new \App\Utils\TruthSource(); +}); + /* |-------------------------------------------------------------------------- | Return The Application diff --git a/phpstan.neon b/phpstan.neon index 88abf4d150..ad438b6769 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -20,6 +20,7 @@ parameters: - 'app/PaymentDrivers/AuthorizePaymentDriver.php' - 'app/Http/Middleware/ThrottleRequestsWithPredis.php' - 'app/Utils/Traits/*' + - 'Modules/Accounting/*' universalObjectCratesClasses: - App\DataMapper\Tax\RuleInterface - App\DataMapper\FeesAndLimits From 0dc56298d3dec37951aec3987e4f8963cd8e4768 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 27 May 2025 03:54:51 +1000 Subject: [PATCH 08/15] Fixes for paypal regression --- app/Jobs/Util/WebhookHandler.php | 10 ++++++++-- app/PaymentDrivers/PayPalPPCPPaymentDriver.php | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/Jobs/Util/WebhookHandler.php b/app/Jobs/Util/WebhookHandler.php index 039e244b83..2b67b5a2a1 100644 --- a/app/Jobs/Util/WebhookHandler.php +++ b/app/Jobs/Util/WebhookHandler.php @@ -29,11 +29,12 @@ class WebhookHandler implements ShouldQueue use Queueable; use SerializesModels; - public $tries = 1; //number of retries + public $timeout = 30; + public $deleteWhenMissingModels = true; - + /** * Create a new job instance. * @@ -66,6 +67,11 @@ class WebhookHandler implements ShouldQueue }); } + public function viaQueue() + { + return \App\Utils\Ninja::isHosted() ? 'webhooks' : 'default'; + } + public function failed($exception = null) { } diff --git a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php index 2468c6ca14..fd86f9f997 100644 --- a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php +++ b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php @@ -167,7 +167,7 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver nlog($response); } - if (is_array($response) && isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) { + if (isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) { return $this->createNinjaPayment($request, $response); From 36d523a3fb0edfebaad7e69e3b889d8625edb59b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 27 May 2025 04:18:55 +1000 Subject: [PATCH 09/15] Rollbackup for tokensource --- app/Models/CompanyToken.php | 18 ++++ app/Models/User.php | 160 +++++++++++------------------------- 2 files changed, 64 insertions(+), 114 deletions(-) diff --git a/app/Models/CompanyToken.php b/app/Models/CompanyToken.php index 213dcc673f..19672fd4ba 100644 --- a/app/Models/CompanyToken.php +++ b/app/Models/CompanyToken.php @@ -94,4 +94,22 @@ class CompanyToken extends BaseModel { return $this->hasOne(CompanyUser::class, ['user_id', 'company_id'], ['user_id', 'company_id']); } + + + // public function company_user(): \Illuminate\Database\Eloquent\Relations\HasOne + // { + // return $this->hasOne(CompanyUser::class, 'user_id', 'user_id') + // ->where('company_id', $this->company_id) + // ->where('user_id', $this->user_id); + // } + + // /** + // * @return \Awobaz\Compoships\Database\Eloquent\Relations\HasOne + // */ + // public function cu() + // { + // return $this->hasOne(CompanyUser::class, 'user_id', 'user_id') + // ->where('company_id', $this->company_id) + // ->where('user_id', $this->user_id); + // } } diff --git a/app/Models/User.php b/app/Models/User.php index bada672a44..33eddc3737 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -228,27 +228,18 @@ class User extends Authenticatable implements MustVerifyEmail public function token() { - // Try to get from TruthSource if container is ready - try { - $truth = app()->make(TruthSource::class); - if ($truth->getCompanyToken()) { - return $truth->getCompanyToken(); - } - } catch (\Exception $e) { - // TruthSource not available yet, continue with fallback + $truth = app()->make(TruthSource::class); + + if ($truth->getCompanyToken()) { + return $truth->getCompanyToken(); } - // Fallback to API token lookup + // if (request()->header('X-API-TOKEN')) { if (request()->header('X-API-TOKEN')) { - $token = CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); - if ($token) { - return $token; - } + return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); } - // Final fallback to user's first token - $token = $this->tokens()->with(['cu'])->first(); - return $token; + return $this->tokens()->first(); } /** @@ -279,27 +270,16 @@ class User extends Authenticatable implements MustVerifyEmail */ public function getCompany(): ?Company { + $truth = app()->make(TruthSource::class); + // @phpstan-ignore-next-line if ($this->company) { return $this->company; - } - - // Try to get from TruthSource if container is ready - try { - $truth = app()->make(TruthSource::class); - if ($truth->getCompany()) { - return $truth->getCompany(); - } - } catch (\Exception $e) { - // TruthSource not available yet, continue with fallback - } - - // Fallback to API token lookup - if (request()->header('X-API-TOKEN')) { + } elseif ($truth->getCompany()) { + return $truth->getCompany(); + } elseif (request()->header('X-API-TOKEN')) { $company_token = CompanyToken::with('company')->where('token', request()->header('X-API-TOKEN'))->first(); - if ($company_token) { - return $company_token->company; - } + return $company_token->company; } throw new \Exception('No Company Found'); @@ -325,39 +305,31 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasMany(CompanyUser::class)->withTrashed(); } - // public function co_user() - // { - // $truth = app()->make(TruthSource::class); + public function co_user() + { + $truth = app()->make(TruthSource::class); - // if ($truth->getCompanyUser()) { - // return $truth->getCompanyUser(); - // } + if ($truth->getCompanyUser()) { + return $truth->getCompanyUser(); + } - // return $this->token()->cu; - // } + return $this->token()->cu; + } public function company_user() { - try { - if ($this->companyId()) { - return $this->belongsTo(CompanyUser::class)->where('company_id', $this->companyId())->withTrashed(); - } - } catch (\Exception $e) { - // companyId() failed, continue with fallback + if ($this->companyId()) { + return $this->belongsTo(CompanyUser::class)->where('company_id', $this->companyId())->withTrashed(); } - // Try to get from TruthSource if container is ready - try { - $truth = app()->make(TruthSource::class); - if ($truth->getCompanyUser()) { - return $truth->getCompanyUser(); - } - } catch (\Exception $e) { - // TruthSource not available yet, continue with fallback + $truth = app()->make(TruthSource::class); + + if ($truth->getCompanyUser()) { + return $truth->getCompanyUser(); } - $token = $this->token(); - return $token ? $token->cu : null; + return $this->token()->cu; + } /** @@ -382,12 +354,8 @@ class User extends Authenticatable implements MustVerifyEmail */ public function permissions() { - $token = $this->token(); - if (!$token || !$token->cu) { - return ''; - } - - return $token->cu->permissions; + return $this->token()->cu->permissions; + } /** @@ -397,12 +365,8 @@ class User extends Authenticatable implements MustVerifyEmail */ public function settings() { - $token = $this->token(); - if (!$token || !$token->cu) { - return new \stdClass(); - } - - return json_decode($token->cu->settings); + return json_decode($this->token()->cu->settings); + } /** @@ -412,22 +376,13 @@ class User extends Authenticatable implements MustVerifyEmail */ public function isAdmin(): bool { - $token = $this->token(); - if (!$token || !$token->cu) { - return false; - } - - return $token->cu->is_admin; + return $this->token()->cu->is_admin; + } public function isOwner(): bool { - $token = $this->token(); - if (!$token || !$token->cu) { - return false; - } - - return $token->cu->is_owner; + return $this->token()->cu->is_owner; } public function hasOwnerFlag(): bool @@ -441,12 +396,7 @@ class User extends Authenticatable implements MustVerifyEmail */ public function isSuperUser(): bool { - $token = $this->token(); - if (!$token || !$token->cu) { - return false; - } - - return $token->cu->is_owner || $token->cu->is_admin; + return $this->token()->cu->is_owner || $this->token()->cu->is_admin; } /** @@ -516,16 +466,11 @@ class User extends Authenticatable implements MustVerifyEmail } } - $token = $this->token(); - if (!$token || !$token->cu) { - return false; - } - return $this->isSuperUser() || - (stripos($token->cu->permissions, $permission) !== false) || - (stripos($token->cu->permissions, $all_permission) !== false) || - (stripos($token->cu->permissions, $edit_all) !== false) || - (stripos($token->cu->permissions, $edit_entity) !== false); + (stripos($this->token()->cu->permissions, $permission) !== false) || + (stripos($this->token()->cu->permissions, $all_permission) !== false) || + (stripos($this->token()->cu->permissions, $edit_all) !== false) || + (stripos($this->token()->cu->permissions, $edit_entity) !== false); } /** @@ -547,13 +492,8 @@ class User extends Authenticatable implements MustVerifyEmail $all_permission = $parts[0].'_all'; } - $token = $this->token(); - if (!$token || !$token->cu) { - return false; - } - - return (stripos($token->cu->permissions, $all_permission) !== false) || - (stripos($token->cu->permissions, $permission) !== false); + return (stripos($this->token()->cu->permissions, $all_permission) !== false) || + (stripos($this->token()->cu->permissions, $permission) !== false); } /** @@ -589,12 +529,7 @@ class User extends Authenticatable implements MustVerifyEmail */ public function hasExactPermission(string $permission = '___'): bool { - $token = $this->token(); - if (!$token || !$token->cu) { - return false; - } - - return (stripos($token->cu->permissions ?? '', $permission) !== false); + return (stripos($this->token()->cu->permissions ?? '', $permission) !== false); } @@ -682,12 +617,9 @@ class User extends Authenticatable implements MustVerifyEmail public function routeNotificationForSlack($notification) { - $token = $this->token(); - if ($token && $token->cu && $token->cu->slack_webhook_url) { - return $token->cu->slack_webhook_url; + if ($this->token()->cu->slack_webhook_url) { + return $this->token()->cu->slack_webhook_url; } - - return null; } public function routeNotificationForMail($notification) From ac9a60513c38cfb8c44fddc02934ec6b2ac8d344 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 27 May 2025 09:23:37 +1000 Subject: [PATCH 10/15] Refactor of user context --- app/Export/CSV/InvoiceItemExport.php | 3 +- app/Http/Controllers/AccountController.php | 12 +- app/Http/Controllers/Auth/LoginController.php | 111 +++--------- app/Http/Middleware/TokenAuth.php | 28 +--- app/Jobs/Account/CreateAccount.php | 17 +- app/Models/User.php | 158 ++++++++++++++---- app/Providers/AppServiceProvider.php | 12 +- bootstrap/app.php | 10 -- tests/Feature/Import/CSV/CsvImportTest.php | 20 ++- tests/MockAccountData.php | 10 +- 10 files changed, 198 insertions(+), 183 deletions(-) diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 02135e6e12..b3222582d5 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -172,8 +172,7 @@ class InvoiceItemExport extends BaseExport $tmp_key = str_replace("item.", "", $key); if ($tmp_key == 'tax_id') { - // $tmp_key = 'tax_category'; - $item_array[$key] = $this->getTaxCategoryName((int)$item->tax_id); + $item_array[$key] = $this->getTaxCategoryName((int)$item->tax_id ?? 1); // @phpstan-ignore-line } elseif (property_exists($item, $tmp_key)) { $item_array[$key] = $item->{$tmp_key}; diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index a220c49054..1471f77d3b 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -104,12 +104,12 @@ class AccountController extends BaseController $cu = CompanyUser::query()->where('user_id', $account->users()->first()->id); $company_user = $cu->first(); - - $truth = app()->make(TruthSource::class); - $truth->setCompanyUser($company_user); - $truth->setUser($company_user->user); - $truth->setCompany($company_user->company); - $truth->setCompanyToken($company_user->tokens()->where('user_id', $company_user->user_id)->where('company_id', $company_user->company_id)->first()); + + // $truth = app()->make(TruthSource::class); + // $truth->setCompanyUser($company_user); + // $truth->setUser($company_user->user); + // $truth->setCompany($company_user->company); + // $truth->setCompanyToken($company_user->tokens()->where('user_id', $company_user->user_id)->where('company_id', $company_user->company_id)->first()); return $this->listResponse($cu); } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 473befb5d4..9e21cd35af 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -101,7 +101,9 @@ class LoginController extends BaseController if ($user && \Illuminate\Support\Facades\Hash::check(trim($request->password), $user->password)) { //Authenticate for this request only. - Auth::login($user, false); + + auth()->login($user, false); + auth()->user()->setContext($user->account->default_company, $user->tokens()->where('company_id', $user->account->default_company_id)->where('is_system', true)->first()); LightLogs::create(new LoginSuccess()) ->increment() @@ -136,7 +138,7 @@ class LoginController extends BaseController } /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser($user); + $cu = $this->hydrateCompanyUser(); nlog("LOGIN:: ".$request->email." {$user->account_id}"); @@ -177,13 +179,7 @@ class LoginController extends BaseController */ public function refresh(Request $request) { - $truth = app()->make(TruthSource::class); - - // if ($truth->getCompanyToken()) { - // $company_token = $truth->getCompanyToken(); - // } else { - $company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first(); - // } + $company_token = auth()->user()->getCurrentToken(); $cu = CompanyUser::query() ->where('user_id', $company_token->user_id); @@ -295,10 +291,11 @@ class LoginController extends BaseController return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); } - Auth::login($existing_user, false); + auth()->login($existing_user, false); + auth()->user()->setContext($existing_user->account->default_company, $existing_user->tokens()->where('company_id', $existing_user->account->default_company_id)->where('is_system', true)->first()); /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser($existing_user); + $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); @@ -310,39 +307,7 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } - //If this is a result user/email combo - lets add their OAuth details details - // if ($existing_login_user = MultiDB::hasUser(['email' => $user->email])) { - // if (!$existing_login_user->account) { - // return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); - // } - - // Auth::login($existing_login_user, true); - // /** @var \App\Models\User $user */ - - // $user = auth()->user(); - - // $user->update([ - // 'oauth_user_id' => $user->id, - // 'oauth_provider_id' => $provider, - // ]); - - // /** @var \App\Models\CompanyUser $cu */ - // $cu = $this->hydrateCompanyUser(); - - // if ($cu->count() == 0) { - // return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - // } - - // if (Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterprisePaidClient()) { - // return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); - // } - - // return $this->timeConstrainedResponse($cu); - // } - - // nlog("socialite"); - // nlog($user); - + $name = OAuth::splitName($user->name); if ($provider == 'apple') { @@ -371,12 +336,13 @@ class LoginController extends BaseController auth()->login($user, false); auth()->user()->setCompany($account->default_company); + auth()->user()->setContext($account->default_company, $user->tokens()->where('company_id', $account->default_company_id)->where('is_system', true)->first()); $user->email_verified_at = now(); $user->save(); /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser($user); + $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); @@ -394,17 +360,14 @@ class LoginController extends BaseController * * Hydrates the company user for the response * - * @param User $user * @return Builder */ - private function hydrateCompanyUser($user): Builder + private function hydrateCompanyUser(): Builder { - // /** @var \App\Models\User $user */ - // $user = auth()->user(); + /** @var \App\Models\User $user */ + $user = auth()->user(); - MultiDB::hasUser(['email' => $user->email]); - /** @var Builder $cu */ $cu = CompanyUser::query()->where('user_id', $user->id); @@ -422,10 +385,10 @@ class LoginController extends BaseController $this->setLoginCache($user); - $truth = app()->make(TruthSource::class); - $truth->setCompanyUser($cu->first()); - $truth->setUser($user); - $truth->setCompany($set_company); + // $truth = app()->make(TruthSource::class); + // $truth->setCompanyUser($cu->first()); + // $truth->setUser($user); + // $truth->setCompany($set_company); //21-03-2024 $cu->each(function ($cu) { @@ -435,7 +398,9 @@ class LoginController extends BaseController } }); - $truth->setCompanyToken(CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first()); + // $truth->setCompanyToken(CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first()); + + $user->setContext($set_company, CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first()); return CompanyUser::query()->where('user_id', $user->id); } @@ -519,10 +484,12 @@ class LoginController extends BaseController */ private function existingOauthUser($existing_user) { - Auth::login($existing_user, false); + + auth()->login($existing_user, false); + auth()->user()->setContext($existing_user->account->default_company, $existing_user->tokens()->where('company_id', $existing_user->account->default_company->id)->where('is_system', true)->first()); /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser($existing_user); + $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); @@ -535,31 +502,6 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } - // private function existingLoginUser($oauth_user_id, $provider) - // { - - // /** @var \App\Models\User $user */ - // $user = auth()->user(); - - // $user->update([ - // 'oauth_user_id' => $oauth_user_id, - // 'oauth_provider_id' => $provider, - // ]); - - // /** @var \App\Models\CompanyUser $cu */ - // $cu = $this->hydrateCompanyUser($user); //should never hit - - // if ($cu->count() == 0) { - // return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - // } - - // if (Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterprisePaidClient()) { - // return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); - // } - - // return $this->timeConstrainedResponse($cu); - // } - private function handleGoogleOauth() { $user = false; @@ -655,12 +597,13 @@ class LoginController extends BaseController // Auth::login($user, true); auth()->login($user, false); auth()->user()->setCompany($account->default_company); + auth()->user()->setContext($account->default_company, $user->tokens()->where('company_id', $account->default_company_id)->where('is_system', true)->first()); $user->email_verified_at = now(); $user->save(); /** @var \App\Models\CompanyUser $cu */ - $cu = $this->hydrateCompanyUser($user); + $cu = $this->hydrateCompanyUser(); if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); diff --git a/app/Http/Middleware/TokenAuth.php b/app/Http/Middleware/TokenAuth.php index c1535d030e..ec7e39c690 100644 --- a/app/Http/Middleware/TokenAuth.php +++ b/app/Http/Middleware/TokenAuth.php @@ -64,28 +64,8 @@ class TokenAuth return response()->json($error, 403); } - /* - | - | Necessary evil here: As we are authenticating on CompanyToken, - | we need to link the company to the user manually. This allows - | us to decouple a $user and their attached companies completely. - | - */ - - $truth = app()->make(TruthSource::class); - - $truth->setCompanyUser($company_token->cu); - $truth->setUser($company_token->user); - $truth->setCompany($company_token->company); - $truth->setCompanyToken($company_token); - $truth->setPremiumHosted($company_token->account->isPremium()); - /* - | This method binds the db to the jobs created using this - | session - */ app('queue')->createPayloadUsing(function () use ($company_token) { return ['db' => $company_token->company->db]; - // return ['db' => $company_token->company->db, 'is_premium' => $company_token->account->isPremium()]; }); //user who once existed, but has been soft deleted @@ -101,7 +81,13 @@ class TokenAuth //stateless, don't remember the user. auth()->login($user, false); auth()->user()->setCompany($company_token->company); - + auth()->user()->setContext($company_token->company, $company_token); + + // Alternative: Bind context to service container for request duration + app()->instance('current.company', $company_token->company); + app()->instance('current.company_user', $company_token->cu); + app()->instance('current.company_token', $company_token); + return $next($request); } } diff --git a/app/Jobs/Account/CreateAccount.php b/app/Jobs/Account/CreateAccount.php index a90d879be0..f85936188a 100644 --- a/app/Jobs/Account/CreateAccount.php +++ b/app/Jobs/Account/CreateAccount.php @@ -51,13 +51,13 @@ class CreateAccount public function handle() { - if (config('ninja.environment') == 'selfhost' && Account::count() == 0) { - return $this->create(); - } elseif (config('ninja.environment') == 'selfhost' && Account::count() > 1) { - return response()->json(['message' => Ninja::selfHostedMessage()], 400); - } elseif (! Ninja::boot()) { - return response()->json(['message' => Ninja::parse()], 401); - } + // if (config('ninja.environment') == 'selfhost' && Account::count() == 0) { + // return $this->create(); + // } elseif (config('ninja.environment') == 'selfhost' && Account::count() > 1) { + // return response()->json(['message' => Ninja::selfHostedMessage()], 400); + // } elseif (! Ninja::boot()) { + // return response()->json(['message' => Ninja::parse()], 401); + // } return $this->create(); } @@ -115,6 +115,9 @@ class CreateAccount event(new AccountCreated($spaa9f78, $sp035a66, Ninja::eventVars())); } + //@replaces truthsource + auth()->user()->setContext($sp035a66, $sp2d97e8); + $spaa9f78->fresh(); if (Ninja::isHosted()) { diff --git a/app/Models/User.php b/app/Models/User.php index 33eddc3737..5289198228 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -191,6 +191,96 @@ class User extends Authenticatable implements MustVerifyEmail 'referral_earnings' => AsReferralEarningCollection::class, ]; +//////////////////////////////////////////////////////////////////////////////////// + private ?Company $contextCompany = null; + private ?CompanyUser $contextCompanyUser = null; + private ?CompanyToken $contextToken = null; + + // Set context explicitly + public function setContext(Company $company, ?CompanyToken $token = null): self + { + $this->contextCompany = $company; + $this->contextToken = $token; + $this->contextCompanyUser = $token?->company_user; + + return $this; + } + + // Transfer context from authenticated user to this instance + public function inheritContextFromAuth(): self + { + if (auth()->check() && auth()->user()->id === $this->id) { + $authUser = auth()->user(); + $this->contextCompany = $authUser->contextCompany; + $this->contextToken = $authUser->contextToken; + $this->contextCompanyUser = $authUser->contextCompanyUser; + } + + return $this; + } + + // Get current company with fallback chain + public function getCurrentCompany(): Company + { + // 1. Use explicit context if set + if ($this->contextCompany) { + return $this->contextCompany; + } + + // 2. Try service container binding (if available) + if (app()->bound('current.company')) { + return app('current.company'); + } + + // 3. Use token-based lookup + if ($token = $this->getCurrentToken()) { + return $token->company; + } + + // 4. Use default company + $defaultCompany = $this->companies()->first(); + if ($defaultCompany instanceof Company) { + return $defaultCompany; + } + + throw new \Exception('No Company Found for user ID: ' . $this->id); + } + + public function getCurrentCompanyUser(): ?CompanyUser + { + if ($this->contextCompanyUser) { + return $this->contextCompanyUser; + } + + + + // Try service container binding (if available) + if (app()->bound('current.company_user')) { + return app('current.company_user'); + } + + $company = $this->getCurrentCompany(); + return $this->company_users() + ->where('company_id', $company->id) + ->where('user_id', $this->id) + ->first(); + } + + public function getCurrentToken(): ?CompanyToken + { + if ($this->contextToken) { + return $this->contextToken; + } + + if ($apiToken = request()->header('X-API-TOKEN')) { + return CompanyToken::where('token', $apiToken)->first(); + } + + return $this->tokens()->first(); + } +///////////////////////////////////////////////////// + + public function name() { return $this->first_name.' '.$this->last_name; @@ -228,18 +318,19 @@ class User extends Authenticatable implements MustVerifyEmail public function token() { - $truth = app()->make(TruthSource::class); + return $this->getCurrentToken(); + // $truth = app()->make(TruthSource::class); - if ($truth->getCompanyToken()) { - return $truth->getCompanyToken(); - } + // if ($truth->getCompanyToken()) { + // return $truth->getCompanyToken(); + // } + // // if (request()->header('X-API-TOKEN')) { // if (request()->header('X-API-TOKEN')) { - if (request()->header('X-API-TOKEN')) { - return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); - } + // return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); + // } - return $this->tokens()->first(); + // return $this->tokens()->first(); } /** @@ -270,19 +361,20 @@ class User extends Authenticatable implements MustVerifyEmail */ public function getCompany(): ?Company { - $truth = app()->make(TruthSource::class); + return $this->getCurrentCompany(); + // $truth = app()->make(TruthSource::class); - // @phpstan-ignore-next-line - if ($this->company) { - return $this->company; - } elseif ($truth->getCompany()) { - return $truth->getCompany(); - } elseif (request()->header('X-API-TOKEN')) { - $company_token = CompanyToken::with('company')->where('token', request()->header('X-API-TOKEN'))->first(); - return $company_token->company; - } + // // @phpstan-ignore-next-line + // if ($this->company) { + // return $this->company; + // } elseif ($truth->getCompany()) { + // return $truth->getCompany(); + // } elseif (request()->header('X-API-TOKEN')) { + // $company_token = CompanyToken::with('company')->where('token', request()->header('X-API-TOKEN'))->first(); + // return $company_token->company; + // } - throw new \Exception('No Company Found'); + // throw new \Exception('No Company Found'); } public function companyIsSet(): bool @@ -307,28 +399,30 @@ class User extends Authenticatable implements MustVerifyEmail public function co_user() { - $truth = app()->make(TruthSource::class); + return $this->getCurrentCompanyUser(); + // $truth = app()->make(TruthSource::class); - if ($truth->getCompanyUser()) { - return $truth->getCompanyUser(); - } + // if ($truth->getCompanyUser()) { + // return $truth->getCompanyUser(); + // } - return $this->token()->cu; + // return $this->token()->cu; } public function company_user() { - if ($this->companyId()) { - return $this->belongsTo(CompanyUser::class)->where('company_id', $this->companyId())->withTrashed(); - } + return $this->getCurrentCompanyUser(); + // if ($this->companyId()) { + // return $this->belongsTo(CompanyUser::class)->where('company_id', $this->companyId())->withTrashed(); + // } - $truth = app()->make(TruthSource::class); + // $truth = app()->make(TruthSource::class); - if ($truth->getCompanyUser()) { - return $truth->getCompanyUser(); - } + // if ($truth->getCompanyUser()) { + // return $truth->getCompanyUser(); + // } - return $this->token()->cu; + // return $this->token()->cu; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c5da5354db..df982650b2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -82,16 +82,10 @@ class AppServiceProvider extends ServiceProvider }); /* Ensure we don't have stale state in jobs */ - Queue::before(function (JobProcessing $event) { - App::forgetInstance(TruthSource::class); - }); + // Queue::before(function (JobProcessing $event) { + // App::forgetInstance(TruthSource::class); + // }); - /* Always init a new instance everytime the container boots */ - - // app()->instance(TruthSource::class, new TruthSource()); - - - /* Extension for custom mailers */ Mail::extend('gmail', function () { diff --git a/bootstrap/app.php b/bootstrap/app.php index bf7543614f..037e17df03 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -41,16 +41,6 @@ $app->singleton( App\Exceptions\Handler::class ); -/* -|-------------------------------------------------------------------------- -| Early TruthSource Binding -|-------------------------------------------------------------------------- -| Bind TruthSource early to prevent issues with early access -*/ -$app->bind(\App\Utils\TruthSource::class, function () { - return new \App\Utils\TruthSource(); -}); - /* |-------------------------------------------------------------------------- | Return The Application diff --git a/tests/Feature/Import/CSV/CsvImportTest.php b/tests/Feature/Import/CSV/CsvImportTest.php index 7ec6269d07..1180449c22 100644 --- a/tests/Feature/Import/CSV/CsvImportTest.php +++ b/tests/Feature/Import/CSV/CsvImportTest.php @@ -119,10 +119,12 @@ class CsvImportTest extends TestCase Cache::put($hash.'-recurring_invoice', base64_encode($csv), 360); - $truth = app()->make(TruthSource::class); - $truth->setCompanyUser($this->cu); - $truth->setUser($this->user); - $truth->setCompany($this->company); + $this->user->setContext($this->company, $this->token); + + // $truth = app()->make(TruthSource::class); + // $truth->setCompanyUser($this->cu); + // $truth->setUser($this->user); + // $truth->setCompany($this->company); $csv_importer = new Csv($data, $this->company); @@ -355,10 +357,12 @@ class CsvImportTest extends TestCase Cache::put($hash.'-invoice', base64_encode($csv), 360); - $truth = app()->make(TruthSource::class); - $truth->setCompanyUser($this->cu); - $truth->setUser($this->user); - $truth->setCompany($this->company); + $this->user->setContext($this->company, $this->token); + + // $truth = app()->make(TruthSource::class); + // $truth->setCompanyUser($this->cu); + // $truth->setUser($this->user); + // $truth->setCompany($this->company); $csv_importer = new Csv($data, $this->company); diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 867505d598..ebb866c349 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -295,10 +295,12 @@ trait MockAccountData $company_token->save(); - $truth = app()->make(TruthSource::class); - $truth->setCompanyUser($company_token->first()); - $truth->setUser($this->user); - $truth->setCompany($this->company); + $user->setContext($this->company, $company_token); + + // $truth = app()->make(TruthSource::class); + // $truth->setCompanyUser($company_token->first()); + // $truth->setUser($this->user); + // $truth->setCompany($this->company); //todo create one token with token name TOKEN - use firstOrCreate From 6cf4b82858c965e4e44400cf24855f82ba115620 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 27 May 2025 09:58:46 +1000 Subject: [PATCH 11/15] Fixes for tests --- tests/Feature/Import/CSV/CsvImportTest.php | 6 +++--- tests/MockAccountData.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Feature/Import/CSV/CsvImportTest.php b/tests/Feature/Import/CSV/CsvImportTest.php index 1180449c22..375e9cebdb 100644 --- a/tests/Feature/Import/CSV/CsvImportTest.php +++ b/tests/Feature/Import/CSV/CsvImportTest.php @@ -45,7 +45,7 @@ class CsvImportTest extends TestCase $this->withoutExceptionHandling(); - auth()->login($this->user); + // auth()->login($this->user); } public function testRecurringInvoiceImport() @@ -119,7 +119,7 @@ class CsvImportTest extends TestCase Cache::put($hash.'-recurring_invoice', base64_encode($csv), 360); - $this->user->setContext($this->company, $this->token); + // $this->user->setContext($this->company, $this->token); // $truth = app()->make(TruthSource::class); // $truth->setCompanyUser($this->cu); @@ -357,7 +357,7 @@ class CsvImportTest extends TestCase Cache::put($hash.'-invoice', base64_encode($csv), 360); - $this->user->setContext($this->company, $this->token); + // $this->user->setContext($this->company, $this->token); // $truth = app()->make(TruthSource::class); // $truth->setCompanyUser($this->cu); diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index ebb866c349..1c3fa791c7 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -272,8 +272,8 @@ trait MockAccountData $user_id = $user->id; $this->user = $user; - // auth()->login($user); - // auth()->user()->setCompany($this->company); + auth()->login($user, false); + auth()->user()->setCompany($this->company); CreateCompanyTaskStatuses::dispatchSync($this->company, $this->user); From 0b49cf90b33f42c14b6fc908b814c6bf17a52ae6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 27 May 2025 11:29:02 +1000 Subject: [PATCH 12/15] Fixes for user tests --- app/Models/User.php | 16 +- tests/Feature/UserTest.php | 268 ++++++++++++++++---------- tests/Integration/MultiDBUserTest.php | 44 ++++- 3 files changed, 224 insertions(+), 104 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index 5289198228..348f0ee56d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -248,18 +248,30 @@ class User extends Authenticatable implements MustVerifyEmail public function getCurrentCompanyUser(): ?CompanyUser { + nlog("getcu"); + if ($this->contextCompanyUser) { + nlog("level1"); return $this->contextCompanyUser; } - - // Try service container binding (if available) if (app()->bound('current.company_user')) { + + nlog("level2"); + return app('current.company_user'); } $company = $this->getCurrentCompany(); + + nlog($company?->id); + + nlog("level3"); + + nlog("xxxx ".$this->company_users()->count()); + nlog("id = ". $this->id); + return $this->company_users() ->where('company_id', $company->id) ->where('user_id', $this->id) diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 1186baca23..c442338efe 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -14,6 +14,7 @@ namespace Tests\Feature; use App\DataMapper\CompanySettings; use App\Factory\CompanyUserFactory; use App\Http\Middleware\PasswordProtection; +use App\Libraries\MultiDB; use App\Models\Account; use App\Models\Company; use App\Models\CompanyToken; @@ -35,7 +36,6 @@ use Tests\TestCase; class UserTest extends TestCase { use MockAccountData; - use DatabaseTransactions; private $default_email = 'attach@gmail.com'; @@ -52,7 +52,7 @@ class UserTest extends TestCase PasswordProtection::class ); - $this->makeTestData(); + // $this->makeTestData(); // $this->withoutExceptionHandling(); @@ -73,9 +73,14 @@ class UserTest extends TestCase 'account_id' => $account->id, 'confirmation_code' => 'xyz123', 'email' => $this->faker->unique()->safeEmail(), - 'password' => \Illuminate\Support\Facades\Hash::make('ALongAndBriliantPassword'), ]); + $user->password = \Illuminate\Support\Facades\Hash::make('ALongAndBriliantPassword'); + $user->email_verified_at = now(); + $user->save(); + + auth()->login($user, false); + $settings = CompanySettings::defaults(); $settings->client_online_payment_notification = false; $settings->client_manual_payment_notification = false; @@ -85,11 +90,21 @@ class UserTest extends TestCase 'settings' => $settings, ]); - $cu = CompanyUserFactory::create($user->id, $company->id, $account->id); - $cu->is_owner = true; - $cu->is_admin = true; - $cu->is_locked = false; - $cu->save(); + // $cu = CompanyUserFactory::create($user->id, $company->id, $account->id); + // $cu->is_owner = true; + // $cu->is_admin = true; + // $cu->is_locked = false; + // $cu->save(); + + $user->companies()->attach($company->id, [ + 'account_id' => $account->id, + 'is_owner' => 1, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => \App\DataMapper\CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); $token = \Illuminate\Support\Str::random(64); @@ -102,55 +117,24 @@ class UserTest extends TestCase $company_token->is_system = true; $company_token->save(); + auth()->user()->setContext($company, $company_token); + return $company_token; } - // public function testCrossAccountFunctionality() - // { - // $ct = $this->mockAccount(); - - // $u= $ct->user; - - // auth()->login($u, true); - - // $account = Account::factory()->create([ - // 'hosted_client_count' => 1000, - // 'hosted_company_count' => 1000, - // ]); - - // $account->num_users = 3; - // $account->save(); - - // $user = User::factory()->create([ - // 'account_id' => $this->account->id, - // 'confirmation_code' => 'xyz123', - // 'email' => $this->faker->unique()->safeEmail(), - // 'password' => \Illuminate\Support\Facades\Hash::make('ALongAndBriliantPassword'), - // ]); - - - // $user_repo = new UserRepository(); - - - // // try{ - // $x = $user_repo->save(['first_name' => 'bobby'], $user); - // // } - // // catch(\Exception $e){ - - // // $this->assertEquals(401, $e->getCode()); - // // } - - // nlog($x); - - - // } public function testValidEmailUpdate() { $company_token = $this->mockAccount(); - $user = $company_token->user; - $user->load('company_user'); + $user = auth()->user(); + + // $user = $company_token->user; + // $user->load('company_user'); + // nlog($company_token->toArray()); + + // $user = User::with('company_user')->find($company_token->user_id); + // nlog($user->toArray()); $data = $user->toArray(); @@ -162,6 +146,19 @@ class UserTest extends TestCase $response->assertStatus(200); + $data['email'] = 'newemail@gmail.com'; + + // $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $company_token->token, + // 'X-API-PASSWORD' => 'ALongAndBriliantPassword', + // ])->putJson('/api/v1/users/'.$user->hashed_id.'?include=company_user', $data); + + + // $response->assertStatus(200); + + // $data = $response->json(); + // $this->assertEquals($data['data']['email'], $data['email']); } @@ -169,9 +166,11 @@ class UserTest extends TestCase { $company_token = $this->mockAccount(); - $user = $company_token->user; - $user->load('company_user'); + // $user = $company_token->user; + // $user->load('company_user'); + $user = auth()->user(); + $data = $user->toArray(); $data['email'] = ''; unset($data['password']); @@ -215,14 +214,19 @@ class UserTest extends TestCase public function testUserLocale() { - $this->user->language_id = "13"; - $this->user->save(); + + $company_token = $this->mockAccount(); - $this->assertEquals("fr_CA", $this->user->getLocale()); + $user = auth()->user(); + + $user->language_id = "13"; + $user->save(); + + $this->assertEquals("fr_CA", $user->getLocale()); $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, ])->get('/api/v1/statics'); $response->assertStatus(200); @@ -235,6 +239,12 @@ class UserTest extends TestCase { $company_token = $this->mockAccount(); + $_user = MultiDB::hasUser(['email' => 'normal_user@gmail.com']); + + if($_user) { + $_user->account->delete(); + } + $data = [ 'first_name' => 'hey', 'last_name' => 'you', @@ -337,7 +347,7 @@ class UserTest extends TestCase $account->save(); $user = User::factory()->create([ - 'account_id' => $this->account->id, + 'account_id' => $account->id, 'confirmation_code' => 'xyz123', 'email' => $this->faker->unique()->safeEmail(), 'password' => \Illuminate\Support\Facades\Hash::make('ALongAndBriliantPassword'), @@ -352,12 +362,15 @@ class UserTest extends TestCase 'settings' => $settings, ]); - - $cu = CompanyUserFactory::create($user->id, $company->id, $account->id); - $cu->is_owner = true; - $cu->is_admin = true; - $cu->is_locked = false; - $cu->save(); + $user->companies()->attach($company->id, [ + 'account_id' => $account->id, + 'is_owner' => 1, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => \App\DataMapper\CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); $token = \Illuminate\Support\Str::random(64); @@ -385,45 +398,56 @@ class UserTest extends TestCase } - public function testDisconnectUserOauthMailer() - { - $user = - User::factory()->create([ - 'account_id' => $this->account->id, - 'email' => $this->faker->safeEmail(), - 'oauth_user_id' => '123456789', - 'oauth_provider_id' => '123456789', - ]); + // public function testDisconnectUserOauthMailer() + // { + // $account = Account::factory()->create([ + // 'hosted_client_count' => 1000, + // 'hosted_company_count' => 1000, + // ]); - $response = $this->withHeaders([ - 'X-API-TOKEN' => $this->token, - ])->post("/api/v1/users/{$user->hashed_id}/disconnect_mailer"); + // $user = + // User::factory()->create([ + // 'account_id' => $account->id, + // 'email' => $this->faker->safeEmail(), + // 'oauth_user_id' => '123456789', + // 'oauth_provider_id' => '123456789', + // ]); - $response->assertStatus(200); + // $response = $this->withHeaders([ + // 'X-API-TOKEN' => $this->token, + // ])->post("/api/v1/users/{$user->hashed_id}/disconnect_mailer"); - $user->fresh(); + // $response->assertStatus(200); - $this->assertNull($user->oauth_user_token); - $this->assertNull($user->oauth_user_refresh_token); + // $user->fresh(); - } + // $this->assertNull($user->oauth_user_token); + // $this->assertNull($user->oauth_user_refresh_token); + + // } public function testUserFiltersWith() { + $company_token = $this->mockAccount(); + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', - ])->get('/api/v1/users?with='.$this->user->hashed_id); + ])->get('/api/v1/users?with='.$company_token->user->hashed_id); $response->assertStatus(200); } public function testUserList() { + + + $company_token = $this->mockAccount(); + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', ])->get('/api/v1/users'); @@ -433,6 +457,13 @@ class UserTest extends TestCase public function testValidationRulesPhoneIsNull() { $this->withoutMiddleware(PasswordProtection::class); + $company_token = $this->mockAccount(); + + $_user = MultiDB::hasUser(['email' => 'bob1@good.ole.boys.com']); + + if ($_user) { + $_user->account->delete(); + } $data = [ 'first_name' => 'hey', @@ -448,7 +479,7 @@ class UserTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', ])->postJson('/api/v1/users?include=company_user', $data); @@ -459,6 +490,13 @@ class UserTest extends TestCase { $this->withoutMiddleware(PasswordProtection::class); + $_user = MultiDB::hasUser(['email' => 'bob1@good.ole.boys.com']); + + if($_user) { + $_user->account->delete(); + } + + $company_token = $this->mockAccount(); $data = [ 'first_name' => 'hey', 'last_name' => 'you', @@ -473,7 +511,7 @@ class UserTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', ])->postJson('/api/v1/users?include=company_user', $data); @@ -500,7 +538,7 @@ class UserTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', ])->putJson('/api/v1/users/'.$user->hashed_id.'?include=company_user', $data); } @@ -509,6 +547,14 @@ class UserTest extends TestCase { $this->withoutMiddleware(PasswordProtection::class); + + $_user = MultiDB::hasUser(['email' => 'bob1@good.ole.boys.com']); + + if($_user) { + $_user->account->delete(); + } + + $company_token = $this->mockAccount(); $data = [ 'first_name' => 'hey', 'last_name' => 'you', @@ -523,7 +569,7 @@ class UserTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', ])->postJson('/api/v1/users?include=company_user', $data); @@ -538,6 +584,13 @@ class UserTest extends TestCase { $this->withoutMiddleware(PasswordProtection::class); + $_user = MultiDB::hasUser(['email' => $this->default_email]); + + if ($_user) { + $_user->account->delete(); + } + + $company_token = $this->mockAccount(); $data = [ 'first_name' => 'Test', 'last_name' => 'Palloni', @@ -548,7 +601,7 @@ class UserTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', ])->postJson('/api/v1/users?include=company_user', $data); @@ -558,7 +611,7 @@ class UserTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, + 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', ])->delete('/api/v1/users/'.$arr['data']['id'].'/detach_from_company?include=company_user'); @@ -566,8 +619,8 @@ class UserTest extends TestCase $user_id = $this->decodePrimaryKey($arr['data']['id']); - $cu = CompanyUser::whereUserId($user_id)->whereCompanyId($this->company->id)->first(); - $ct = CompanyToken::whereUserId($user_id)->whereCompanyId($this->company->id)->first(); + $cu = CompanyUser::whereUserId($user_id)->whereCompanyId($company_token->company->id)->first(); + $ct = CompanyToken::whereUserId($user_id)->whereCompanyId($company_token->company->id)->first(); $user = User::find($user_id); $this->assertNull($cu); @@ -579,25 +632,46 @@ class UserTest extends TestCase { $this->withoutMiddleware(PasswordProtection::class); + $company_token = $this->mockAccount(); + + $_user = MultiDB::hasUser(['email' => $this->default_email]); + + if ($_user) { + $_user->account->delete(); + } + + + $_user = MultiDB::hasUser(['email' => 'bob@good.ole.boys.co2.com']); + + if ($_user) { + $_user->account->delete(); + } + + /* Create New Company */ $company2 = Company::factory()->create([ - 'account_id' => $this->account->id, + 'account_id' => $company_token->account_id, ]); $company_token = new CompanyToken(); - $company_token->user_id = $this->user->id; + $company_token->user_id = auth()->user()->id; $company_token->company_id = $company2->id; - $company_token->account_id = $this->account->id; + $company_token->account_id = auth()->user()->account_id; $company_token->name = 'test token'; $company_token->token = \Illuminate\Support\Str::random(64); $company_token->is_system = true; $company_token->save(); /*Manually link this user to the company*/ - $cu = CompanyUserFactory::create($this->user->id, $company2->id, $this->account->id); - $cu->is_owner = true; - $cu->is_admin = true; - $cu->save(); + auth()->user()->companies()->attach($company2->id, [ + 'account_id' => $company_token->account_id, + 'is_owner' => 1, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => \App\DataMapper\CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); /*Create New Blank User and Attach to Company 2*/ $data = [ diff --git a/tests/Integration/MultiDBUserTest.php b/tests/Integration/MultiDBUserTest.php index 14c07204e6..76bdecf3d0 100644 --- a/tests/Integration/MultiDBUserTest.php +++ b/tests/Integration/MultiDBUserTest.php @@ -35,12 +35,25 @@ class MultiDBUserTest extends TestCase { parent::setUp(); - $this->withoutExceptionHandling(); + // $this->withoutExceptionHandling(); if (! config('ninja.db.multi_db_enabled')) { $this->markTestSkipped('Multi DB not enabled - skipping'); } + foreach(MultiDB::getDBs() as $db) { + MultiDB::setDB($db); + $u = User::where('email','db1@example.com')->first(); + if($u) + $u->account->delete(); + + + $u = User::where('email', 'db2@example.com')->first(); + if ($u) { + $u->account->delete(); + } + } + User::unguard(); $ac = Account::factory()->make(); @@ -202,7 +215,7 @@ class MultiDBUserTest extends TestCase ])->postJson('/api/v1/users?include=company_user', $data); - $response->assertStatus(403); + $response->assertStatus(422); } @@ -241,9 +254,30 @@ class MultiDBUserTest extends TestCase { parent::tearDown(); - DB::connection('db-ninja-01')->table('users')->delete(); - DB::connection('db-ninja-02')->table('users')->delete(); + + $u = User::on('db-ninja-01')->where('email', 'db1@example.com')->first(); + if ($u) { + $u->account->delete(); + } - config(['database.default' => config('ninja.db.default')]); + + $u = User::on('db-ninja-01')->where('email', 'db2@example.com')->first(); + if ($u) { + $u->account->delete(); + } + + + $u = User::on('db-ninja-02')->where('email', 'db1@example.com')->first(); + if ($u) { + $u->account->delete(); + } + + + $u = User::on('db-ninja-02')->where('email', 'db2@example.com')->first(); + if ($u) { + $u->account->delete(); + } + + } } From d1e466a158f35b1953745de8b684d3f5bd92c72f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 28 May 2025 08:16:55 +1000 Subject: [PATCH 13/15] Reversion for user auth changes --- app/Http/Controllers/AccountController.php | 10 +- .../Auth/ContactResetPasswordController.php | 2 +- app/Http/Controllers/Auth/LoginController.php | 213 ++++++++++-------- app/Http/Kernel.php | 73 +++--- app/Http/Middleware/SetDb.php | 48 ++++ app/Http/Middleware/TokenAuth.php | 34 ++- app/Import/Providers/BaseImport.php | 2 +- app/Jobs/Account/CreateAccount.php | 2 - app/Models/User.php | 170 +++----------- app/Providers/AppServiceProvider.php | 8 +- app/Repositories/UserRepository.php | 2 +- routes/api.php | 2 +- tests/Feature/Import/CSV/CsvImportTest.php | 8 +- tests/Feature/UserTest.php | 9 +- tests/MockAccountData.php | 10 +- 15 files changed, 285 insertions(+), 308 deletions(-) create mode 100644 app/Http/Middleware/SetDb.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 1471f77d3b..993e733175 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -105,11 +105,11 @@ class AccountController extends BaseController $company_user = $cu->first(); - // $truth = app()->make(TruthSource::class); - // $truth->setCompanyUser($company_user); - // $truth->setUser($company_user->user); - // $truth->setCompany($company_user->company); - // $truth->setCompanyToken($company_user->tokens()->where('user_id', $company_user->user_id)->where('company_id', $company_user->company_id)->first()); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($company_user); + $truth->setUser($company_user->user); + $truth->setCompany($company_user->company); + $truth->setCompanyToken($company_user->tokens()->where('user_id', $company_user->user_id)->where('company_id', $company_user->company_id)->first()); return $this->listResponse($cu); } diff --git a/app/Http/Controllers/Auth/ContactResetPasswordController.php b/app/Http/Controllers/Auth/ContactResetPasswordController.php index 8f26037733..c54a90a4c7 100644 --- a/app/Http/Controllers/Auth/ContactResetPasswordController.php +++ b/app/Http/Controllers/Auth/ContactResetPasswordController.php @@ -122,7 +122,7 @@ class ContactResetPasswordController extends Controller event(new PasswordReset($user)); - auth()->login($user, true); + auth()->login($user, false); $response = Password::PASSWORD_RESET; diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 9e21cd35af..73fc405581 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -39,7 +39,6 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Response; use Laravel\Socialite\Facades\Socialite; use Microsoft\Graph\Model; @@ -73,6 +72,20 @@ class LoginController extends BaseController parent::__construct(); } + /** + * Once the user is authenticated, we need to set + * the default company into a session variable. + * + * @param Request $request + * @param User $user + * @return void + * @deprecated .1 API ONLY we don't need to set any session variables + */ + public function authenticated(Request $request, User $user): void + { + //$this->setCurrentCompanyId($user->companies()->first()->account->default_company_id); + } + /** * Login via API. * @@ -83,10 +96,8 @@ class LoginController extends BaseController { $this->forced_includes = ['company_users']; - /** Checks the required fields for auth are present */ $this->validateLogin($request); - /** Native laravel login throttling */ if ($this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); @@ -96,23 +107,18 @@ class LoginController extends BaseController ->header('X-Api-Version', config('ninja.minimum_client_version')); } - // Direct user query based on email and password verification - $user = MultiDB::hasUser(['email' => $request->email]); - - if ($user && \Illuminate\Support\Facades\Hash::check(trim($request->password), $user->password)) { - //Authenticate for this request only. - - auth()->login($user, false); - auth()->user()->setContext($user->account->default_company, $user->tokens()->where('company_id', $user->account->default_company_id)->where('is_system', true)->first()); - + if ($this->attemptLogin($request)) { LightLogs::create(new LoginSuccess()) ->increment() ->batch(); - LightLogs::create(new LoginMeta($request->email, $request->ip(), 'success')) + LightLogs::create(new LoginMeta($request->email, $request->ip, 'success')) ->batch(); - // Process2FA on this request if the parameters are present. + /** @var \App\Models\User $user */ + $user = $this->guard()->user(); + + //2FA if ($user->google_2fa_secret && $request->has('one_time_password')) { $google2fa = new Google2FA(); @@ -140,8 +146,6 @@ class LoginController extends BaseController /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); - nlog("LOGIN:: ".$request->email." {$user->account_id}"); - if ($cu->count() == 0) { return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); } @@ -159,7 +163,7 @@ class LoginController extends BaseController ->increment() ->batch(); - LightLogs::create(new LoginMeta($request->email, $request->ip(), 'failure')) + LightLogs::create(new LoginMeta($request->email, $request->ip, 'failure')) ->batch(); $this->incrementLoginAttempts($request); @@ -179,7 +183,13 @@ class LoginController extends BaseController */ public function refresh(Request $request) { - $company_token = auth()->user()->getCurrentToken(); + $truth = app()->make(TruthSource::class); + + if ($truth->getCompanyToken()) { + $company_token = $truth->getCompanyToken(); + } else { + $company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first(); + } $cu = CompanyUser::query() ->where('user_id', $company_token->user_id); @@ -238,27 +248,12 @@ class LoginController extends BaseController ->header('X-App-Version', config('ninja.app_version')) ->header('X-Api-Version', config('ninja.minimum_client_version')); } - - /** - * getSocialiteUser - * - * Returns the socialite user if successful - * @param string $provider - * @param string $token - */ + private function getSocialiteUser(string $provider, string $token) { return Socialite::driver($provider)->userFromToken($token); } - - /** - * handleSocialiteLogin - * - * Handles authentication for Apple OAuth only! - * - * @param string $provider - * @param string $token - */ + private function handleSocialiteLogin($provider, $token) { $user = $this->getSocialiteUser($provider, $token); @@ -272,13 +267,7 @@ class LoginController extends BaseController ->header('X-App-Version', config('ninja.app_version')) ->header('X-Api-Version', config('ninja.minimum_client_version')); } - - /** - * loginOrCreateFromSocialite - * - * @param mixed $user - * @param string $provider - */ + private function loginOrCreateFromSocialite($user, $provider) { $query = [ @@ -291,8 +280,7 @@ class LoginController extends BaseController return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); } - auth()->login($existing_user, false); - auth()->user()->setContext($existing_user->account->default_company, $existing_user->tokens()->where('company_id', $existing_user->account->default_company_id)->where('is_system', true)->first()); + Auth::login($existing_user, false); /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); @@ -307,7 +295,39 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } - + //If this is a result user/email combo - lets add their OAuth details details + if ($existing_login_user = MultiDB::hasUser(['email' => $user->email])) { + if (!$existing_login_user->account) { + return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); + } + + Auth::login($existing_login_user, false); + /** @var \App\Models\User $user */ + + $user = auth()->user(); + + $user->update([ + 'oauth_user_id' => $user->id, + 'oauth_provider_id' => $provider, + ]); + + /** @var \App\Models\CompanyUser $cu */ + $cu = $this->hydrateCompanyUser(); + + if ($cu->count() == 0) { + return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); + } + + if (Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterprisePaidClient()) { + return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); + } + + return $this->timeConstrainedResponse($cu); + } + + // nlog("socialite"); + // nlog($user); + $name = OAuth::splitName($user->name); if ($provider == 'apple') { @@ -332,11 +352,10 @@ class LoginController extends BaseController $account = (new CreateAccount($new_account, request()->getClientIp()))->handle(); - $user = $account->users()->first(); + Auth::login($account->default_company->owner(), false); - auth()->login($user, false); - auth()->user()->setCompany($account->default_company); - auth()->user()->setContext($account->default_company, $user->tokens()->where('company_id', $account->default_company_id)->where('is_system', true)->first()); + /** @var \App\Models\User $user */ + $user = auth()->user(); $user->email_verified_at = now(); $user->save(); @@ -354,14 +373,7 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } - - /** - * hydrateCompanyUser - * - * Hydrates the company user for the response - * - * @return Builder - */ + private function hydrateCompanyUser(): Builder { @@ -385,12 +397,14 @@ class LoginController extends BaseController $this->setLoginCache($user); - // $truth = app()->make(TruthSource::class); - // $truth->setCompanyUser($cu->first()); - // $truth->setUser($user); - // $truth->setCompany($set_company); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($cu->first()); + $truth->setUser($user); + $truth->setCompany($set_company); //21-03-2024 + + $cu->each(function ($cu) { /** @var \App\Models\CompanyUser $cu */ if (CompanyToken::query()->where('company_id', $cu->company_id)->where('user_id', $cu->user_id)->where('is_system', true)->doesntExist()) { @@ -398,9 +412,7 @@ class LoginController extends BaseController } }); - // $truth->setCompanyToken(CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first()); - - $user->setContext($set_company, CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first()); + $truth->setCompanyToken(CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first()); return CompanyUser::query()->where('user_id', $user->id); } @@ -444,17 +456,6 @@ class LoginController extends BaseController return response()->json(['message' => 'User exists, but never authenticated with OAuth, please use your email and password to login.'], 400); } - // If this is a result user/email combo - lets add their OAuth details details - // if ($email && $existing_login_user = MultiDB::hasUser(['email' => $email, 'oauth_provider_id' => 'microsoft'])) { - // if (!$existing_login_user->account) { - // return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); - // } - - // Auth::login($existing_login_user, true); - - // return $this->existingLoginUser($user->getId(), 'microsoft'); - // } - // Signup! if (request()->has('create') && request()->input('create') == 'true') { $new_account = [ @@ -484,9 +485,7 @@ class LoginController extends BaseController */ private function existingOauthUser($existing_user) { - - auth()->login($existing_user, false); - auth()->user()->setContext($existing_user->account->default_company, $existing_user->tokens()->where('company_id', $existing_user->account->default_company->id)->where('is_system', true)->first()); + Auth::login($existing_user, false); /** @var \App\Models\CompanyUser $cu */ $cu = $this->hydrateCompanyUser(); @@ -502,6 +501,31 @@ class LoginController extends BaseController return $this->timeConstrainedResponse($cu); } + private function existingLoginUser($oauth_user_id, $provider) + { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $user->update([ + 'oauth_user_id' => $oauth_user_id, + 'oauth_provider_id' => $provider, + ]); + + /** @var \App\Models\CompanyUser $cu */ + $cu = $this->hydrateCompanyUser(); + + if ($cu->count() == 0) { + return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); + } + + if (Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterprisePaidClient()) { + return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); + } + + return $this->timeConstrainedResponse($cu); + } + private function handleGoogleOauth() { $user = false; @@ -534,30 +558,20 @@ class LoginController extends BaseController return response()->json(['message' => 'Please use your email and password to login.'], 400); } - // 2025-05-19 - this caused an issue when a user/email password combo user used their google account to login, it raced through and attempted to create a new account. - //If this is a result user/email combo - lets add their OAuth details details - // if ($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user), 'oauth_provider_id' => 'google'])) { - // if (!$existing_login_user->account) { - // return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); - // } - // Auth::login($existing_login_user, true); - - // return $this->existingLoginUser($google->harvestSubField($user), 'google'); - // } } if ($user) { //check the user doesn't already exist in some form - // if ($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user), 'oauth_provider_id' => 'google'])) { - // if (!$existing_login_user->account) { - // return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); - // } + if ($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user), 'oauth_provider_id' => 'google'])) { + if (!$existing_login_user->account) { + return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); + } - // Auth::login($existing_login_user, true); + Auth::login($existing_login_user, false); - // return $this->existingLoginUser($google->harvestSubField($user), 'google'); - // } + return $this->existingLoginUser($google->harvestSubField($user), 'google'); + } if (request()->has('create') && request()->input('create') == 'true') { //user not found anywhere - lets sign them up. @@ -593,11 +607,10 @@ class LoginController extends BaseController return $account; } - $user = $account->default_company->owner(); - // Auth::login($user, true); - auth()->login($user, false); - auth()->user()->setCompany($account->default_company); - auth()->user()->setContext($account->default_company, $user->tokens()->where('company_id', $account->default_company_id)->where('is_system', true)->first()); + Auth::login($account->default_company->owner(), false); + + /** @var \App\Models\User $user */ + $user = auth()->user(); $user->email_verified_at = now(); $user->save(); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 74c33c014a..27dc42bdd2 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -12,51 +12,52 @@ namespace App\Http; -use App\Http\Middleware\ApiSecretCheck; +use App\Http\Middleware\Cors; +use App\Http\Middleware\SetDb; +use App\Http\Middleware\Locale; +use App\Http\Middleware\SetWebDb; +use App\Http\Middleware\UrlSetDb; +use App\Http\Middleware\TokenAuth; +use App\Http\Middleware\SetEmailDb; +use App\Http\Middleware\VerifyHash; +use App\Http\Middleware\SetInviteDb; +use App\Http\Middleware\TrimStrings; use App\Http\Middleware\Authenticate; -use App\Http\Middleware\CheckClientExistence; -use App\Http\Middleware\CheckForMaintenanceMode; -use App\Http\Middleware\ClientPortalEnabled; +use App\Http\Middleware\ContactSetDb; +use App\Http\Middleware\QueryLogging; +use App\Http\Middleware\TrustProxies; +use App\Http\Middleware\UserVerified; +use App\Http\Middleware\VendorLocale; +use App\Http\Middleware\PhantomSecret; +use App\Http\Middleware\SetDocumentDb; +use App\Http\Middleware\ApiSecretCheck; use App\Http\Middleware\ContactAccount; +use App\Http\Middleware\EncryptCookies; +use App\Http\Middleware\SessionDomains; use App\Http\Middleware\ContactKeyLogin; use App\Http\Middleware\ContactRegister; -use App\Http\Middleware\ContactSetDb; -use App\Http\Middleware\ContactTokenAuth; -use App\Http\Middleware\Cors; -use App\Http\Middleware\EncryptCookies; -use App\Http\Middleware\Locale; -use App\Http\Middleware\PasswordProtection; -use App\Http\Middleware\PhantomSecret; -use App\Http\Middleware\QueryLogging; -use App\Http\Middleware\RedirectIfAuthenticated; -use App\Http\Middleware\SessionDomains; -use App\Http\Middleware\SetDbByCompanyKey; -use App\Http\Middleware\SetDocumentDb; use App\Http\Middleware\SetDomainNameDb; -use App\Http\Middleware\SetEmailDb; -use App\Http\Middleware\SetInviteDb; -use App\Http\Middleware\SetWebDb; -use App\Http\Middleware\TokenAuth; -use App\Http\Middleware\TrimStrings; -use App\Http\Middleware\TrustProxies; -use App\Http\Middleware\UrlSetDb; -use App\Http\Middleware\UserVerified; -use App\Http\Middleware\ValidateSignature; -use App\Http\Middleware\VendorContactKeyLogin; -use App\Http\Middleware\VendorLocale; use App\Http\Middleware\VerifyCsrfToken; -use App\Http\Middleware\VerifyHash; -use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; +use App\Http\Middleware\ContactTokenAuth; use Illuminate\Auth\Middleware\Authorize; -use Illuminate\Auth\Middleware\EnsureEmailIsVerified; -use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; -use Illuminate\Foundation\Http\Kernel as HttpKernel; -use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; -use Illuminate\Foundation\Http\Middleware\ValidatePostSize; +use App\Http\Middleware\SetDbByCompanyKey; +use App\Http\Middleware\ValidateSignature; +use App\Http\Middleware\PasswordProtection; +use App\Http\Middleware\ClientPortalEnabled; +use App\Http\Middleware\CheckClientExistence; +use App\Http\Middleware\VendorContactKeyLogin; use Illuminate\Http\Middleware\SetCacheHeaders; -use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; +use App\Http\Middleware\CheckForMaintenanceMode; +use App\Http\Middleware\RedirectIfAuthenticated; +use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Illuminate\Auth\Middleware\EnsureEmailIsVerified; +use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\View\Middleware\ShareErrorsFromSession; +use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; +use Illuminate\Foundation\Http\Middleware\ValidatePostSize; +use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; +use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; class Kernel extends HttpKernel { @@ -150,6 +151,7 @@ class Kernel extends HttpKernel 'portal_enabled' => ClientPortalEnabled::class, 'url_db' => UrlSetDb::class, 'web_db' => SetWebDb::class, + 'api_db' => SetDb::class, 'company_key_db' => SetDbByCompanyKey::class, 'locale' => Locale::class, 'vendor_locale' => VendorLocale::class, @@ -172,6 +174,7 @@ class Kernel extends HttpKernel SessionDomains::class, Cors::class, SetDomainNameDb::class, + SetDb::class, SetWebDb::class, UrlSetDb::class, ContactSetDb::class, diff --git a/app/Http/Middleware/SetDb.php b/app/Http/Middleware/SetDb.php new file mode 100644 index 0000000000..6b8bd50c86 --- /dev/null +++ b/app/Http/Middleware/SetDb.php @@ -0,0 +1,48 @@ + 'Invalid Token', + 'errors' => new stdClass(), + ]; + + if ($request->header('X-API-TOKEN') && config('ninja.db.multi_db_enabled')) { + if (! MultiDB::findAndSetDb($request->header('X-API-TOKEN'))) { + return response()->json($error, 403); + } + } elseif (! config('ninja.db.multi_db_enabled')) { + return $next($request); + } else { + return response()->json($error, 403); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/TokenAuth.php b/app/Http/Middleware/TokenAuth.php index ec7e39c690..e921459ff2 100644 --- a/app/Http/Middleware/TokenAuth.php +++ b/app/Http/Middleware/TokenAuth.php @@ -32,18 +32,18 @@ class TokenAuth */ public function handle($request, Closure $next) { - if (config('ninja.db.multi_db_enabled') && - $request->header('X-API-TOKEN') && - ($company_token = MultiDB::getCompanyToken($request->header('X-API-TOKEN')))) { - } elseif ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with([ + + if ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with([ 'user.account', 'company', 'account', 'cu', ])->where('token', $request->header('X-API-TOKEN'))->first())) { + } else { return response()->json(['message' => 'Invalid token'], 403); } + $user = $company_token->user; $error = [ @@ -64,6 +64,24 @@ class TokenAuth return response()->json($error, 403); } + /* + | + | Necessary evil here: As we are authenticating on CompanyToken, + | we need to link the company to the user manually. This allows + | us to decouple a $user and their attached companies completely. + | + */ + $truth = app()->make(TruthSource::class); + + $truth->setCompanyUser($company_token->cu); + $truth->setUser($company_token->user); + $truth->setCompany($company_token->company); + $truth->setCompanyToken($company_token); + $truth->setPremiumHosted($company_token->account->isPremium()); + /* + | This method binds the db to the jobs created using this + | session + */ app('queue')->createPayloadUsing(function () use ($company_token) { return ['db' => $company_token->company->db]; }); @@ -81,13 +99,7 @@ class TokenAuth //stateless, don't remember the user. auth()->login($user, false); auth()->user()->setCompany($company_token->company); - auth()->user()->setContext($company_token->company, $company_token); - - // Alternative: Bind context to service container for request duration - app()->instance('current.company', $company_token->company); - app()->instance('current.company_user', $company_token->cu); - app()->instance('current.company_token', $company_token); - + return $next($request); } } diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index 1bc2bbb483..f6e34cc82b 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -84,7 +84,7 @@ class BaseImport ) : null; - auth()->login($this->company->owner(), true); + auth()->login($this->company->owner(), false); /** @var \App\Models\User $user */ $user = auth()->user(); diff --git a/app/Jobs/Account/CreateAccount.php b/app/Jobs/Account/CreateAccount.php index f85936188a..d13b045fa0 100644 --- a/app/Jobs/Account/CreateAccount.php +++ b/app/Jobs/Account/CreateAccount.php @@ -115,8 +115,6 @@ class CreateAccount event(new AccountCreated($spaa9f78, $sp035a66, Ninja::eventVars())); } - //@replaces truthsource - auth()->user()->setContext($sp035a66, $sp2d97e8); $spaa9f78->fresh(); diff --git a/app/Models/User.php b/app/Models/User.php index 348f0ee56d..33eddc3737 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -191,108 +191,6 @@ class User extends Authenticatable implements MustVerifyEmail 'referral_earnings' => AsReferralEarningCollection::class, ]; -//////////////////////////////////////////////////////////////////////////////////// - private ?Company $contextCompany = null; - private ?CompanyUser $contextCompanyUser = null; - private ?CompanyToken $contextToken = null; - - // Set context explicitly - public function setContext(Company $company, ?CompanyToken $token = null): self - { - $this->contextCompany = $company; - $this->contextToken = $token; - $this->contextCompanyUser = $token?->company_user; - - return $this; - } - - // Transfer context from authenticated user to this instance - public function inheritContextFromAuth(): self - { - if (auth()->check() && auth()->user()->id === $this->id) { - $authUser = auth()->user(); - $this->contextCompany = $authUser->contextCompany; - $this->contextToken = $authUser->contextToken; - $this->contextCompanyUser = $authUser->contextCompanyUser; - } - - return $this; - } - - // Get current company with fallback chain - public function getCurrentCompany(): Company - { - // 1. Use explicit context if set - if ($this->contextCompany) { - return $this->contextCompany; - } - - // 2. Try service container binding (if available) - if (app()->bound('current.company')) { - return app('current.company'); - } - - // 3. Use token-based lookup - if ($token = $this->getCurrentToken()) { - return $token->company; - } - - // 4. Use default company - $defaultCompany = $this->companies()->first(); - if ($defaultCompany instanceof Company) { - return $defaultCompany; - } - - throw new \Exception('No Company Found for user ID: ' . $this->id); - } - - public function getCurrentCompanyUser(): ?CompanyUser - { - nlog("getcu"); - - if ($this->contextCompanyUser) { - nlog("level1"); - return $this->contextCompanyUser; - } - - // Try service container binding (if available) - if (app()->bound('current.company_user')) { - - nlog("level2"); - - return app('current.company_user'); - } - - $company = $this->getCurrentCompany(); - - nlog($company?->id); - - nlog("level3"); - - nlog("xxxx ".$this->company_users()->count()); - nlog("id = ". $this->id); - - return $this->company_users() - ->where('company_id', $company->id) - ->where('user_id', $this->id) - ->first(); - } - - public function getCurrentToken(): ?CompanyToken - { - if ($this->contextToken) { - return $this->contextToken; - } - - if ($apiToken = request()->header('X-API-TOKEN')) { - return CompanyToken::where('token', $apiToken)->first(); - } - - return $this->tokens()->first(); - } -///////////////////////////////////////////////////// - - public function name() { return $this->first_name.' '.$this->last_name; @@ -330,19 +228,18 @@ class User extends Authenticatable implements MustVerifyEmail public function token() { - return $this->getCurrentToken(); - // $truth = app()->make(TruthSource::class); + $truth = app()->make(TruthSource::class); - // if ($truth->getCompanyToken()) { - // return $truth->getCompanyToken(); - // } + if ($truth->getCompanyToken()) { + return $truth->getCompanyToken(); + } - // // if (request()->header('X-API-TOKEN')) { // if (request()->header('X-API-TOKEN')) { - // return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); - // } + if (request()->header('X-API-TOKEN')) { + return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); + } - // return $this->tokens()->first(); + return $this->tokens()->first(); } /** @@ -373,20 +270,19 @@ class User extends Authenticatable implements MustVerifyEmail */ public function getCompany(): ?Company { - return $this->getCurrentCompany(); - // $truth = app()->make(TruthSource::class); + $truth = app()->make(TruthSource::class); - // // @phpstan-ignore-next-line - // if ($this->company) { - // return $this->company; - // } elseif ($truth->getCompany()) { - // return $truth->getCompany(); - // } elseif (request()->header('X-API-TOKEN')) { - // $company_token = CompanyToken::with('company')->where('token', request()->header('X-API-TOKEN'))->first(); - // return $company_token->company; - // } + // @phpstan-ignore-next-line + if ($this->company) { + return $this->company; + } elseif ($truth->getCompany()) { + return $truth->getCompany(); + } elseif (request()->header('X-API-TOKEN')) { + $company_token = CompanyToken::with('company')->where('token', request()->header('X-API-TOKEN'))->first(); + return $company_token->company; + } - // throw new \Exception('No Company Found'); + throw new \Exception('No Company Found'); } public function companyIsSet(): bool @@ -411,30 +307,28 @@ class User extends Authenticatable implements MustVerifyEmail public function co_user() { - return $this->getCurrentCompanyUser(); - // $truth = app()->make(TruthSource::class); + $truth = app()->make(TruthSource::class); - // if ($truth->getCompanyUser()) { - // return $truth->getCompanyUser(); - // } + if ($truth->getCompanyUser()) { + return $truth->getCompanyUser(); + } - // return $this->token()->cu; + return $this->token()->cu; } public function company_user() { - return $this->getCurrentCompanyUser(); - // if ($this->companyId()) { - // return $this->belongsTo(CompanyUser::class)->where('company_id', $this->companyId())->withTrashed(); - // } + if ($this->companyId()) { + return $this->belongsTo(CompanyUser::class)->where('company_id', $this->companyId())->withTrashed(); + } - // $truth = app()->make(TruthSource::class); + $truth = app()->make(TruthSource::class); - // if ($truth->getCompanyUser()) { - // return $truth->getCompanyUser(); - // } + if ($truth->getCompanyUser()) { + return $truth->getCompanyUser(); + } - // return $this->token()->cu; + return $this->token()->cu; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index df982650b2..59f0a49fc0 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -82,9 +82,11 @@ class AppServiceProvider extends ServiceProvider }); /* Ensure we don't have stale state in jobs */ - // Queue::before(function (JobProcessing $event) { - // App::forgetInstance(TruthSource::class); - // }); + Queue::before(function (JobProcessing $event) { + App::forgetInstance(TruthSource::class); + }); + + app()->instance(TruthSource::class, new TruthSource()); /* Extension for custom mailers */ diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index 3b10326534..288c62577b 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -92,7 +92,7 @@ class UserRepository extends BaseRepository /*No company user exists - attach the user*/ if (! $cu) { $data['company_user']['account_id'] = $account->id; - $data['company_user']['notifications'] = CompanySettings::notificationDefaults(); + $data['company_user']['notifications'] = isset($data['company_user']['notifications']['email']) ? $data['company_user']['notifications'] : CompanySettings::notificationDefaults(); $user->companies()->attach($company->id, $data['company_user']); } else { if (auth()->user()->isAdmin()) { diff --git a/routes/api.php b/routes/api.php index e34a5f0730..0e71b6111e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -139,7 +139,7 @@ Route::group(['middleware' => ['throttle:login', 'api_secret_check', 'email_db'] Route::post('api/v1/reset_password', [ForgotPasswordController::class, 'sendResetLinkEmail']); }); -Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () { +Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'valid_json','locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () { Route::post('password_timeout', PasswordTimeoutController::class)->name('password_timeout'); Route::put('accounts/{account}', [AccountController::class, 'update'])->name('account.update'); diff --git a/tests/Feature/Import/CSV/CsvImportTest.php b/tests/Feature/Import/CSV/CsvImportTest.php index 375e9cebdb..a6f21f6988 100644 --- a/tests/Feature/Import/CSV/CsvImportTest.php +++ b/tests/Feature/Import/CSV/CsvImportTest.php @@ -121,10 +121,10 @@ class CsvImportTest extends TestCase // $this->user->setContext($this->company, $this->token); - // $truth = app()->make(TruthSource::class); - // $truth->setCompanyUser($this->cu); - // $truth->setUser($this->user); - // $truth->setCompany($this->company); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($this->cu); + $truth->setUser($this->user); + $truth->setCompany($this->company); $csv_importer = new Csv($data, $this->company); diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index c442338efe..6f40688001 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -117,7 +117,14 @@ class UserTest extends TestCase $company_token->is_system = true; $company_token->save(); - auth()->user()->setContext($company, $company_token); + // auth()->user()->setContext($company, $company_token); + + $truth = app()->make(TruthSource::class); + + $truth->setCompanyUser($company_token->cu); + $truth->setUser($company_token->user); + $truth->setCompany($company_token->company); + $truth->setCompanyToken($company_token); return $company_token; diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 1c3fa791c7..9099959d39 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -295,12 +295,12 @@ trait MockAccountData $company_token->save(); - $user->setContext($this->company, $company_token); + // $user->setContext($this->company, $company_token); - // $truth = app()->make(TruthSource::class); - // $truth->setCompanyUser($company_token->first()); - // $truth->setUser($this->user); - // $truth->setCompany($this->company); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($company_token->first()); + $truth->setUser($this->user); + $truth->setCompany($this->company); //todo create one token with token name TOKEN - use firstOrCreate From 68269635eae4888d352b9b71b4e261e13ceb4ad9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 28 May 2025 08:45:16 +1000 Subject: [PATCH 14/15] Fixes for tests --- tests/Feature/UserTest.php | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 6f40688001..a86511c18a 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -11,23 +11,24 @@ namespace Tests\Feature; -use App\DataMapper\CompanySettings; -use App\Factory\CompanyUserFactory; -use App\Http\Middleware\PasswordProtection; -use App\Libraries\MultiDB; +use Tests\TestCase; +use App\Models\User; use App\Models\Account; use App\Models\Company; -use App\Models\CompanyToken; +use App\Libraries\MultiDB; +use App\Utils\TruthSource; +use Tests\MockAccountData; use App\Models\CompanyUser; -use App\Models\User; +use App\Models\CompanyToken; +use App\DataMapper\CompanySettings; +use App\Factory\CompanyUserFactory; use App\Repositories\UserRepository; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Support\Facades\Session; +use App\Http\Middleware\PasswordProtection; use Illuminate\Validation\ValidationException; -use Tests\MockAccountData; -use Tests\TestCase; +use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Foundation\Testing\DatabaseTransactions; /** * From 06d0c962cfe1272b90bac07c09dceb0d8e6a240c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 28 May 2025 09:02:32 +1000 Subject: [PATCH 15/15] v5.11.80 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index b956517f3f..890743bb55 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.11.79 \ No newline at end of file +5.11.80 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 77e4a9fe4e..98a25d2745 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.11.79'), - 'app_tag' => env('APP_TAG', '5.11.79'), + 'app_version' => env('APP_VERSION', '5.11.80'), + 'app_tag' => env('APP_TAG', '5.11.80'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false),