Refactor for loading company user relation

This commit is contained in:
David Bomba 2025-05-26 16:26:03 +10:00
parent c8b9c3682b
commit e2988a8c71
14 changed files with 203 additions and 92 deletions

View File

@ -296,6 +296,20 @@ class InvoiceFilters extends QueryFilters
return $this->builder->orderByRaw("REGEXP_REPLACE(invoices.number,'[^0-9]+','')+0 " . $dir); 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); return $this->builder->orderBy("{$this->builder->getQuery()->from}.".$sort_col[0], $dir);
} }

View File

@ -73,20 +73,6 @@ class LoginController extends BaseController
parent::__construct(); 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. * Login via API.
* *
@ -97,8 +83,10 @@ class LoginController extends BaseController
{ {
$this->forced_includes = ['company_users']; $this->forced_includes = ['company_users'];
/** Checks the required fields for auth are present */
$this->validateLogin($request); $this->validateLogin($request);
/** Native laravel login throttling */
if ($this->hasTooManyLoginAttempts($request)) { if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request); $this->fireLockoutEvent($request);
@ -112,17 +100,17 @@ class LoginController extends BaseController
$user = MultiDB::hasUser(['email' => $request->email]); $user = MultiDB::hasUser(['email' => $request->email]);
if ($user && \Illuminate\Support\Facades\Hash::check(trim($request->password), $user->password)) { 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); Auth::login($user, false);
LightLogs::create(new LoginSuccess()) LightLogs::create(new LoginSuccess())
->increment() ->increment()
->batch(); ->batch();
LightLogs::create(new LoginMeta($request->email, $request->ip, 'success')) LightLogs::create(new LoginMeta($request->email, $request->ip(), 'success'))
->batch(); ->batch();
//2FA // Process2FA on this request if the parameters are present.
if ($user->google_2fa_secret && $request->has('one_time_password')) { if ($user->google_2fa_secret && $request->has('one_time_password')) {
$google2fa = new Google2FA(); $google2fa = new Google2FA();
@ -150,6 +138,8 @@ class LoginController extends BaseController
/** @var \App\Models\CompanyUser $cu */ /** @var \App\Models\CompanyUser $cu */
$cu = $this->hydrateCompanyUser($user); $cu = $this->hydrateCompanyUser($user);
nlog("LOGIN:: ".$request->email." {$user->account_id}");
if ($cu->count() == 0) { if ($cu->count() == 0) {
return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); 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() ->increment()
->batch(); ->batch();
LightLogs::create(new LoginMeta($request->email, $request->ip, 'failure')) LightLogs::create(new LoginMeta($request->email, $request->ip(), 'failure'))
->batch(); ->batch();
$this->incrementLoginAttempts($request); $this->incrementLoginAttempts($request);
@ -189,11 +179,11 @@ class LoginController extends BaseController
{ {
$truth = app()->make(TruthSource::class); $truth = app()->make(TruthSource::class);
if ($truth->getCompanyToken()) { // if ($truth->getCompanyToken()) {
$company_token = $truth->getCompanyToken(); // $company_token = $truth->getCompanyToken();
} else { // } else {
$company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first(); $company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first();
} // }
$cu = CompanyUser::query() $cu = CompanyUser::query()
->where('user_id', $company_token->user_id); ->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-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_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) private function getSocialiteUser(string $provider, string $token)
{ {
return Socialite::driver($provider)->userFromToken($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) private function handleSocialiteLogin($provider, $token)
{ {
$user = $this->getSocialiteUser($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-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version')); ->header('X-Api-Version', config('ninja.minimum_client_version'));
} }
/**
* loginOrCreateFromSocialite
*
* @param mixed $user
* @param string $provider
*/
private function loginOrCreateFromSocialite($user, $provider) private function loginOrCreateFromSocialite($user, $provider)
{ {
$query = [ $query = [
@ -356,16 +367,11 @@ class LoginController extends BaseController
$account = (new CreateAccount($new_account, request()->getClientIp()))->handle(); $account = (new CreateAccount($new_account, request()->getClientIp()))->handle();
$user = $account->default_company->owner(); $user = $account->users()->first();
// Auth::login($user, false);
auth()->login($user, false); auth()->login($user, false);
auth()->user()->setCompany($account->default_company); auth()->user()->setCompany($account->default_company);
// /** @var \App\Models\User $user */
// $user = auth()->user();
$user->email_verified_at = now(); $user->email_verified_at = now();
$user->save(); $user->save();
@ -382,7 +388,15 @@ class LoginController extends BaseController
return $this->timeConstrainedResponse($cu); return $this->timeConstrainedResponse($cu);
} }
/**
* hydrateCompanyUser
*
* Hydrates the company user for the response
*
* @param User $user
* @return Builder
*/
private function hydrateCompanyUser($user): Builder private function hydrateCompanyUser($user): Builder
{ {
@ -414,8 +428,6 @@ class LoginController extends BaseController
$truth->setCompany($set_company); $truth->setCompany($set_company);
//21-03-2024 //21-03-2024
$cu->each(function ($cu) { $cu->each(function ($cu) {
/** @var \App\Models\CompanyUser $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()) { 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) private function existingOauthUser($existing_user)
{ {
Auth::login($existing_user, true); Auth::login($existing_user, false);
/** @var \App\Models\CompanyUser $cu */ /** @var \App\Models\CompanyUser $cu */
$cu = $this->hydrateCompanyUser($existing_user); $cu = $this->hydrateCompanyUser($existing_user);

View File

@ -39,6 +39,7 @@ class TokenAuth
'user.account', 'user.account',
'company', 'company',
'account', 'account',
'cu',
])->where('token', $request->header('X-API-TOKEN'))->first())) { ])->where('token', $request->header('X-API-TOKEN'))->first())) {
} else { } else {
return response()->json(['message' => 'Invalid token'], 403); return response()->json(['message' => 'Invalid token'], 403);
@ -70,6 +71,7 @@ class TokenAuth
| us to decouple a $user and their attached companies completely. | us to decouple a $user and their attached companies completely.
| |
*/ */
$truth = app()->make(TruthSource::class); $truth = app()->make(TruthSource::class);
$truth->setCompanyUser($company_token->cu); $truth->setCompanyUser($company_token->cu);

View File

@ -84,9 +84,7 @@ class CompanyToken extends BaseModel
public function company_user(): \Illuminate\Database\Eloquent\Relations\HasOne public function company_user(): \Illuminate\Database\Eloquent\Relations\HasOne
{ {
return $this->hasOne(CompanyUser::class, 'user_id', 'user_id') return $this->hasOne(CompanyUser::class, ['user_id', 'company_id'], ['user_id', 'company_id']);
->where('company_id', $this->company_id)
->where('user_id', $this->user_id);
} }
/** /**
@ -94,8 +92,6 @@ class CompanyToken extends BaseModel
*/ */
public function cu() public function cu()
{ {
return $this->hasOne(CompanyUser::class, 'user_id', 'user_id') return $this->hasOne(CompanyUser::class, ['user_id', 'company_id'], ['user_id', 'company_id']);
->where('company_id', $this->company_id)
->where('user_id', $this->user_id);
} }
} }

View File

@ -153,6 +153,7 @@ class CreditInvitation extends BaseModel
public function markViewed() public function markViewed()
{ {
$this->viewed_date = Carbon::now(); $this->viewed_date = Carbon::now();
$this->credit->last_viewed = Carbon::now();
$this->save(); $this->save();
} }

View File

@ -271,7 +271,7 @@ class Invoice extends BaseModel
'custom_value4' => (string)$this->custom_value4, 'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key, 'company_key' => $this->company->company_key,
'po_number' => (string)$this->po_number, 'po_number' => (string)$this->po_number,
'line_items' => $this->line_items, // 'line_items' => $this->line_items,
]; ];
} }

View File

@ -148,6 +148,7 @@ class InvoiceInvitation extends BaseModel
public function markViewed(): void public function markViewed(): void
{ {
$this->viewed_date = Carbon::now(); $this->viewed_date = Carbon::now();
$this->invoice->last_viewed = Carbon::now();
$this->save(); $this->save();
} }

View File

@ -139,6 +139,7 @@ class PurchaseOrderInvitation extends BaseModel
public function markViewed(): void public function markViewed(): void
{ {
$this->viewed_date = Carbon::now(); $this->viewed_date = Carbon::now();
$this->purchase_order->last_viewed = Carbon::now();
$this->save(); $this->save();
} }

View File

@ -139,6 +139,7 @@ class QuoteInvitation extends BaseModel
public function markViewed() public function markViewed()
{ {
$this->viewed_date = Carbon::now(); $this->viewed_date = Carbon::now();
$this->quote->last_viewed = Carbon::now();
$this->save(); $this->save();
} }

View File

@ -228,18 +228,27 @@ class User extends Authenticatable implements MustVerifyEmail
public function token() public function token()
{ {
$truth = app()->make(TruthSource::class); // Try to get from TruthSource if container is ready
try {
if ($truth->getCompanyToken()) { $truth = app()->make(TruthSource::class);
return $truth->getCompanyToken(); 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')) { 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 public function getCompany(): ?Company
{ {
$truth = app()->make(TruthSource::class);
// @phpstan-ignore-next-line // @phpstan-ignore-next-line
if ($this->company) { if ($this->company) {
return $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(); $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'); throw new \Exception('No Company Found');
@ -305,31 +325,39 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->hasMany(CompanyUser::class)->withTrashed(); return $this->hasMany(CompanyUser::class)->withTrashed();
} }
public function co_user() // public function co_user()
{ // {
$truth = app()->make(TruthSource::class); // $truth = app()->make(TruthSource::class);
if ($truth->getCompanyUser()) { // if ($truth->getCompanyUser()) {
return $truth->getCompanyUser(); // return $truth->getCompanyUser();
} // }
return $this->token()->cu; // return $this->token()->cu;
} // }
public function company_user() public function company_user()
{ {
if ($this->companyId()) { try {
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();
}
} catch (\Exception $e) {
// companyId() failed, continue with fallback
} }
$truth = app()->make(TruthSource::class); // Try to get from TruthSource if container is ready
try {
if ($truth->getCompanyUser()) { $truth = app()->make(TruthSource::class);
return $truth->getCompanyUser(); 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() 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() 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 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 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 public function hasOwnerFlag(): bool
@ -396,7 +441,12 @@ class User extends Authenticatable implements MustVerifyEmail
*/ */
public function isSuperUser(): bool 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() || return $this->isSuperUser() ||
(stripos($this->token()->cu->permissions, $permission) !== false) || (stripos($token->cu->permissions, $permission) !== false) ||
(stripos($this->token()->cu->permissions, $all_permission) !== false) || (stripos($token->cu->permissions, $all_permission) !== false) ||
(stripos($this->token()->cu->permissions, $edit_all) !== false) || (stripos($token->cu->permissions, $edit_all) !== false) ||
(stripos($this->token()->cu->permissions, $edit_entity) !== false); (stripos($token->cu->permissions, $edit_entity) !== false);
} }
/** /**
@ -492,8 +547,13 @@ class User extends Authenticatable implements MustVerifyEmail
$all_permission = $parts[0].'_all'; $all_permission = $parts[0].'_all';
} }
return (stripos($this->token()->cu->permissions, $all_permission) !== false) || $token = $this->token();
(stripos($this->token()->cu->permissions, $permission) !== false); 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 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) public function routeNotificationForSlack($notification)
{ {
if ($this->token()->cu->slack_webhook_url) { $token = $this->token();
return $this->token()->cu->slack_webhook_url; if ($token && $token->cu && $token->cu->slack_webhook_url) {
return $token->cu->slack_webhook_url;
} }
return null;
} }
public function routeNotificationForMail($notification) public function routeNotificationForMail($notification)

View File

@ -83,12 +83,15 @@ class AppServiceProvider extends ServiceProvider
/* Ensure we don't have stale state in jobs */ /* Ensure we don't have stale state in jobs */
Queue::before(function (JobProcessing $event) { Queue::before(function (JobProcessing $event) {
App::forgetInstance('truthsource'); App::forgetInstance(TruthSource::class);
}); });
/* Always init a new instance everytime the container boots */ /* 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 */ /* Extension for custom mailers */
Mail::extend('gmail', function () { Mail::extend('gmail', function () {
@ -155,5 +158,6 @@ class AppServiceProvider extends ServiceProvider
public function register(): void public function register(): void
{ {
} }
} }

View File

@ -119,7 +119,7 @@ class ActivityRepository extends BaseRepository
public function getTokenId(array $event_vars) public function getTokenId(array $event_vars)
{ {
if ($event_vars['token']) { if (isset($event_vars['token']) &&$event_vars['token']) {
/** @var \App\Models\CompanyToken $company_token **/ /** @var \App\Models\CompanyToken $company_token **/
$company_token = CompanyToken::query()->where('token', $event_vars['token'])->first(); $company_token = CompanyToken::query()->where('token', $event_vars['token'])->first();

View File

@ -41,6 +41,16 @@ $app->singleton(
App\Exceptions\Handler::class 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 | Return The Application

View File

@ -20,6 +20,7 @@ parameters:
- 'app/PaymentDrivers/AuthorizePaymentDriver.php' - 'app/PaymentDrivers/AuthorizePaymentDriver.php'
- 'app/Http/Middleware/ThrottleRequestsWithPredis.php' - 'app/Http/Middleware/ThrottleRequestsWithPredis.php'
- 'app/Utils/Traits/*' - 'app/Utils/Traits/*'
- 'Modules/Accounting/*'
universalObjectCratesClasses: universalObjectCratesClasses:
- App\DataMapper\Tax\RuleInterface - App\DataMapper\Tax\RuleInterface
- App\DataMapper\FeesAndLimits - App\DataMapper\FeesAndLimits