Refactor of user context

This commit is contained in:
David Bomba 2025-05-27 09:23:37 +10:00
parent 36d523a3fb
commit ac9a60513c
10 changed files with 198 additions and 183 deletions

View File

@ -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};

View File

@ -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);
}

View File

@ -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,38 +307,6 @@ 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);
@ -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,16 +360,13 @@ 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();
MultiDB::hasUser(['email' => $user->email]);
/** @var \App\Models\User $user */
$user = auth()->user();
/** @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);

View File

@ -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,6 +81,12 @@ 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);
}

View File

@ -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()) {

View File

@ -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;
}

View File

@ -82,15 +82,9 @@ class AppServiceProvider extends ServiceProvider
});
/* Ensure we don't have stale state in jobs */
Queue::before(function (JobProcessing $event) {
App::forgetInstance(TruthSource::class);
});
/* Always init a new instance everytime the container boots */
// app()->instance(TruthSource::class, new TruthSource());
// Queue::before(function (JobProcessing $event) {
// App::forgetInstance(TruthSource::class);
// });
/* Extension for custom mailers */

View File

@ -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

View File

@ -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);

View File

@ -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