From 36c34fa302575bb863b7bec070c88fd4f7ddc192 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 04:50:19 +0000 Subject: [PATCH 1/3] Redirect to ivplv2 company dashboard after login After authentication, both elevated and regular users are now sent to the ivplv2 company dashboard. For elevated users the ivplv2 company is looked up by search_code; for regular users it is preferred from their attached companies. Both paths fall back gracefully when ivplv2 is absent. Six PHPUnit tests cover the redirect behaviour across role types and company attachment scenarios. https://claude.ai/code/session_01KVV91ECY14zwgavRm99zPs --- .../Core/Filament/Responses/LoginResponse.php | 42 ++-- .../Core/Tests/Feature/LoginRedirectTest.php | 233 ++++++++++++++++++ 2 files changed, 253 insertions(+), 22 deletions(-) create mode 100644 Modules/Core/Tests/Feature/LoginRedirectTest.php diff --git a/Modules/Core/Filament/Responses/LoginResponse.php b/Modules/Core/Filament/Responses/LoginResponse.php index 97e506a4e..c2c2633ef 100644 --- a/Modules/Core/Filament/Responses/LoginResponse.php +++ b/Modules/Core/Filament/Responses/LoginResponse.php @@ -5,39 +5,39 @@ use Filament\Auth\Http\Responses\Contracts\LoginResponse as BaseLoginResponse; use Illuminate\Support\Str; use Modules\Core\Enums\UserRole; +use Modules\Core\Models\Company; class LoginResponse implements BaseLoginResponse { + private const DEFAULT_COMPANY_CODE = 'ivplv2'; + public function toResponse($request): mixed { - $user = auth()->user(); - $elevatedRoles = UserRole::elevated(); - $isElevated = false; - - foreach ($elevatedRoles as $role) { - if ($user->hasRole($role)) { - $isElevated = true; - break; - } - } + $user = auth()->user(); + $isElevated = collect(UserRole::elevated()) + ->contains(fn ($role) => $user->hasRole($role)); if ($isElevated) { - $tenant = \Modules\Core\Models\Company::query()->first(); // <<whereRaw('LOWER(search_code) = ?', [self::DEFAULT_COMPANY_CODE]) + ->first() + ?? Company::query()->first(); + + if (! $tenant) { abort(500, 'Fallback company not found.'); } + + filament()->setTenant($tenant); } else { - $tenant = $user->companies()->first(); - if ( ! $tenant) { + $tenant = $user->companies() + ->whereRaw('LOWER(search_code) = ?', [self::DEFAULT_COMPANY_CODE]) + ->first() + ?? $user->companies()->first(); + + if (! $tenant) { abort(500, 'No company found for this user.'); } - } - // For super_admins or Filament panel routes (with {tenant}), do not set session, only set Filament tenant - if ($isElevated) { - filament()->setTenant($tenant); - } else { - // For regular users, set both session and Filament tenant session(['current_company_id' => $tenant->id]); filament()->setTenant($tenant); } @@ -45,7 +45,5 @@ public function toResponse($request): mixed return redirect()->route('filament.company.pages.dashboard', [ 'tenant' => Str::lower($tenant->search_code), ]); - - //return redirect()->intended(Filament::getUrl()); } } diff --git a/Modules/Core/Tests/Feature/LoginRedirectTest.php b/Modules/Core/Tests/Feature/LoginRedirectTest.php new file mode 100644 index 000000000..815241a2a --- /dev/null +++ b/Modules/Core/Tests/Feature/LoginRedirectTest.php @@ -0,0 +1,233 @@ +setCurrentPanel(filament()->getPanel('company')); + } + + protected function tearDown(): void + { + Carbon::setTestNow(); + parent::tearDown(); + } + + private function activeUser(array $overrides = []): User + { + return User::factory()->create(array_merge([ + 'is_active' => true, + 'email_verified_at' => Carbon::now(), + 'password' => bcrypt('password'), + ], $overrides)); + } + + private function ivplv2Company(): Company + { + return Company::factory()->create([ + 'search_code' => 'ivplv2', + 'name' => 'InvoicePlane Corporation', + 'slug' => 'invoiceplane-corporation', + ]); + } + + private function elevatedRole(string $role): void + { + Role::query()->firstOrCreate(['name' => $role, 'guard_name' => 'web']); + } + + # region elevated users + + #[Test] + #[Group('authentication')] + #[Group('redirect')] + public function it_redirects_elevated_user_to_ivplv2_dashboard_after_login(): void + { + /* Arrange */ + $this->elevatedRole(UserRole::SUPER_ADMIN->value); + $this->ivplv2Company(); + + $user = $this->activeUser(['email' => 'super@example.com']); + $user->assignRole(UserRole::SUPER_ADMIN->value); + + /* Act */ + $response = Livewire::test(Login::class) + ->fillForm([ + 'email' => 'super@example.com', + 'password' => 'password', + ]) + ->call('authenticate'); + + /* Assert */ + $response->assertRedirect( + route('filament.company.pages.dashboard', ['tenant' => 'ivplv2']) + ); + $this->assertAuthenticated(); + } + + #[Test] + #[Group('authentication')] + #[Group('redirect')] + public function it_redirects_admin_user_to_ivplv2_dashboard_after_login(): void + { + /* Arrange */ + $this->elevatedRole(UserRole::ADMIN->value); + $this->ivplv2Company(); + + $user = $this->activeUser(['email' => 'admin@example.com']); + $user->assignRole(UserRole::ADMIN->value); + + /* Act */ + $response = Livewire::test(Login::class) + ->fillForm([ + 'email' => 'admin@example.com', + 'password' => 'password', + ]) + ->call('authenticate'); + + /* Assert */ + $response->assertRedirect( + route('filament.company.pages.dashboard', ['tenant' => 'ivplv2']) + ); + } + + #[Test] + #[Group('authentication')] + #[Group('redirect')] + public function it_falls_back_to_first_company_when_ivplv2_absent_for_elevated_user(): void + { + /* Arrange */ + $this->elevatedRole(UserRole::SUPER_ADMIN->value); + $otherCompany = Company::factory()->create(['search_code' => 'acme']); + + $user = $this->activeUser(['email' => 'super@example.com']); + $user->assignRole(UserRole::SUPER_ADMIN->value); + + /* Act */ + $response = Livewire::test(Login::class) + ->fillForm([ + 'email' => 'super@example.com', + 'password' => 'password', + ]) + ->call('authenticate'); + + /* Assert */ + $response->assertRedirect( + route('filament.company.pages.dashboard', ['tenant' => 'acme']) + ); + } + + # endregion + + # region regular users + + #[Test] + #[Group('authentication')] + #[Group('redirect')] + public function it_redirects_regular_user_to_ivplv2_when_attached_to_it(): void + { + /* Arrange */ + $this->elevatedRole(UserRole::CUSTOMER_ADMIN->value); + $ivplv2 = $this->ivplv2Company(); + + $user = $this->activeUser(['email' => 'client@example.com']); + $user->assignRole(UserRole::CUSTOMER_ADMIN->value); + $user->companies()->attach($ivplv2->id); + + /* Act */ + $response = Livewire::test(Login::class) + ->fillForm([ + 'email' => 'client@example.com', + 'password' => 'password', + ]) + ->call('authenticate'); + + /* Assert */ + $response->assertRedirect( + route('filament.company.pages.dashboard', ['tenant' => 'ivplv2']) + ); + $this->assertAuthenticated(); + } + + #[Test] + #[Group('authentication')] + #[Group('redirect')] + public function it_prefers_ivplv2_over_other_companies_for_regular_user(): void + { + /* Arrange */ + $this->elevatedRole(UserRole::CUSTOMER_ADMIN->value); + $other = Company::factory()->create(['search_code' => 'acme']); + $ivplv2 = $this->ivplv2Company(); + + $user = $this->activeUser(['email' => 'client@example.com']); + $user->assignRole(UserRole::CUSTOMER_ADMIN->value); + // Attach other company first — ivplv2 should still win + $user->companies()->attach($other->id); + $user->companies()->attach($ivplv2->id); + + /* Act */ + $response = Livewire::test(Login::class) + ->fillForm([ + 'email' => 'client@example.com', + 'password' => 'password', + ]) + ->call('authenticate'); + + /* Assert */ + $response->assertRedirect( + route('filament.company.pages.dashboard', ['tenant' => 'ivplv2']) + ); + } + + #[Test] + #[Group('authentication')] + #[Group('redirect')] + public function it_falls_back_to_first_company_when_regular_user_is_not_attached_to_ivplv2(): void + { + /* Arrange */ + $this->elevatedRole(UserRole::CUSTOMER_ADMIN->value); + $otherCompany = Company::factory()->create(['search_code' => 'acme']); + + $user = $this->activeUser(['email' => 'client@example.com']); + $user->assignRole(UserRole::CUSTOMER_ADMIN->value); + $user->companies()->attach($otherCompany->id); + + /* Act */ + $response = Livewire::test(Login::class) + ->fillForm([ + 'email' => 'client@example.com', + 'password' => 'password', + ]) + ->call('authenticate'); + + /* Assert */ + $response->assertRedirect( + route('filament.company.pages.dashboard', ['tenant' => 'acme']) + ); + } + + # endregion +} From 6c3b466bcb0da4d0764f5d284c507d929ba3b222 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:48:03 +0000 Subject: [PATCH 2/3] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit --- Modules/Core/Filament/Responses/LoginResponse.php | 8 ++++---- Modules/Core/Tests/Feature/LoginRedirectTest.php | 8 ++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Modules/Core/Filament/Responses/LoginResponse.php b/Modules/Core/Filament/Responses/LoginResponse.php index c2c2633ef..55273ab00 100644 --- a/Modules/Core/Filament/Responses/LoginResponse.php +++ b/Modules/Core/Filament/Responses/LoginResponse.php @@ -21,10 +21,10 @@ public function toResponse($request): mixed $tenant = Company::query() ->whereRaw('LOWER(search_code) = ?', [self::DEFAULT_COMPANY_CODE]) ->first() - ?? Company::query()->first(); + ?? Company::query()->oldest('id')->first(); if (! $tenant) { - abort(500, 'Fallback company not found.'); + abort(500, trans('auth.fallback_company_not_found')); } filament()->setTenant($tenant); @@ -32,10 +32,10 @@ public function toResponse($request): mixed $tenant = $user->companies() ->whereRaw('LOWER(search_code) = ?', [self::DEFAULT_COMPANY_CODE]) ->first() - ?? $user->companies()->first(); + ?? $user->companies()->oldest('id')->first(); if (! $tenant) { - abort(500, 'No company found for this user.'); + abort(500, trans('auth.no_company_found_for_user')); } session(['current_company_id' => $tenant->id]); diff --git a/Modules/Core/Tests/Feature/LoginRedirectTest.php b/Modules/Core/Tests/Feature/LoginRedirectTest.php index 815241a2a..11382fa2b 100644 --- a/Modules/Core/Tests/Feature/LoginRedirectTest.php +++ b/Modules/Core/Tests/Feature/LoginRedirectTest.php @@ -2,8 +2,6 @@ namespace Modules\Core\Tests\Feature; -use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Support\Carbon; use Livewire\Livewire; use Modules\Core\Enums\UserRole; @@ -11,17 +9,15 @@ use Modules\Core\Filament\Responses\LoginResponse; use Modules\Core\Models\Company; use Modules\Core\Models\User; -use Modules\Core\Tests\CreatesApplication; +use Modules\Core\Tests\AbstractCompanyPanelTestCase; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Spatie\Permission\Models\Role; #[CoversClass(LoginResponse::class)] -class LoginRedirectTest extends BaseTestCase +class LoginRedirectTest extends AbstractCompanyPanelTestCase { - use CreatesApplication; - use RefreshDatabase; protected function setUp(): void { From 388309f527ef9c4ab0770e631d5d5b1cb88b861f Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:00:50 +0000 Subject: [PATCH 3/3] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- Modules/Core/Tests/Feature/LoginRedirectTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/Core/Tests/Feature/LoginRedirectTest.php b/Modules/Core/Tests/Feature/LoginRedirectTest.php index 11382fa2b..c8348f77c 100644 --- a/Modules/Core/Tests/Feature/LoginRedirectTest.php +++ b/Modules/Core/Tests/Feature/LoginRedirectTest.php @@ -22,6 +22,8 @@ class LoginRedirectTest extends AbstractCompanyPanelTestCase protected function setUp(): void { parent::setUp(); + // Clean slate: remove companies created by base class to properly test fallback logic + Company::query()->delete(); Carbon::setTestNow(Carbon::parse('2026-01-01 00:00:00')); filament()->setCurrentPanel(filament()->getPanel('company')); } @@ -117,7 +119,7 @@ public function it_falls_back_to_first_company_when_ivplv2_absent_for_elevated_u { /* Arrange */ $this->elevatedRole(UserRole::SUPER_ADMIN->value); - $otherCompany = Company::factory()->create(['search_code' => 'acme']); + Company::factory()->create(['search_code' => 'acme']); $user = $this->activeUser(['email' => 'super@example.com']); $user->assignRole(UserRole::SUPER_ADMIN->value);