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