Skip to content

Commit

Permalink
Merge pull request #76 from bramr94/multi-tenancy
Browse files Browse the repository at this point in the history
Multi-tenancy support
  • Loading branch information
bert-w authored Mar 1, 2024
2 parents c7e42bd + baee6e8 commit 5f9196d
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 10 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@ class SocialiteUser implements FilamentSocialiteUserContract
}
```

### Change login redirect

When your panel has [multi-tenancy](https://filamentphp.com/docs/3.x/panels/tenancy) enabled, after logging in, the user will be redirected to their [default tenant](https://filamentphp.com/docs/3.x/panels/tenancy#setting-the-default-tenant).
If you want to change this behavior, you can add the `setLoginRedirectCallback` method in the boot method of your `AppServiceProvider.php`:

```php
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use DutchCodingCompany\FilamentSocialite\Models\SocialiteUser;

FilamentSocialite::setLoginRedirectCallback(function (string $provider, FilamentSocialiteUserContract $socialiteUser) {
return redirect()->intended(
route(FilamentSocialite::getPlugin()->getDashboardRouteName())
);
});
```

### Filament Fortify

This component can also be added while using the [Fortify plugin](https://filamentphp.com/plugins/fortify) plugin.
Expand Down
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ parameters:
count: 2
path: tests/SocialiteLoginTest.php

-
message: "#^Call to an undefined method Mockery\\\\ExpectationInterface\\|Mockery\\\\HigherOrderMessage\\:\\:andReturn\\(\\)\\.$#"
count: 1
path: tests/SocialiteTenantLoginTest.php

-
message: "#^Cannot call method run\\(\\) on Illuminate\\\\Testing\\\\PendingCommand\\|int\\.$#"
count: 1
Expand Down
2 changes: 1 addition & 1 deletion src/Facades/FilamentSocialite.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Illuminate\Support\Facades\Facade;

/**
* @see \DutchCodingCompany\FilamentSocialite\FilamentSocialite
* @mixin \DutchCodingCompany\FilamentSocialite\FilamentSocialite
*/
class FilamentSocialite extends Facade
{
Expand Down
40 changes: 40 additions & 0 deletions src/FilamentSocialite.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use DutchCodingCompany\FilamentSocialite\Exceptions\ProviderNotConfigured;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Filament\Facades\Filament;
use Filament\Panel;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Factory;
use Illuminate\Contracts\Auth\StatefulGuard;
Expand All @@ -32,6 +33,11 @@ class FilamentSocialite
*/
protected ?Closure $createUserCallback = null;

/**
* @phpstan-var ?\Closure(string $provider, \DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser $socialiteUser): \Illuminate\Http\RedirectResponse
*/
protected ?Closure $loginRedirectCallback = null;

protected FilamentSocialitePlugin $plugin;

public function __construct(
Expand Down Expand Up @@ -208,4 +214,38 @@ public function getGuard(): StatefulGuard

throw GuardNotStateful::make($guardName);
}

/**
* @param \Closure(string $provider, \DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser $socialiteUser): \Illuminate\Http\RedirectResponse $callback
*/
public function setLoginRedirectCallback(Closure $callback): static
{
$this->loginRedirectCallback = $callback;

return $this;
}

/**
* @return \Closure(string $provider, \DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser $socialiteUser): \Illuminate\Http\RedirectResponse
*/
public function getLoginRedirectCallback(): Closure
{
return $this->loginRedirectCallback ?? function (string $provider, FilamentSocialiteUserContract $socialiteUser) {
if (($panel = Filament::getCurrentPanel())->hasTenancy()) {
$tenant = Filament::getUserDefaultTenant($socialiteUser->getUser());

if (is_null($tenant) && $tenantRegistrationUrl = $panel->getTenantRegistrationUrl()) {
return redirect()->intended($tenantRegistrationUrl);
}

return redirect()->intended(
$panel->getUrl($tenant)
);
}

return redirect()->intended(
route($this->getPlugin()->getDashboardRouteName())
);
};
}
}
13 changes: 5 additions & 8 deletions src/Http/Controllers/SocialiteLoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,15 @@ protected function isUserAllowed(SocialiteUserContract $user): bool
return in_array($emailDomain, $domains);
}

protected function loginUser(FilamentSocialiteUserContract $socialiteUser): RedirectResponse
protected function loginUser(string $provider, FilamentSocialiteUserContract $socialiteUser): RedirectResponse
{
// Log the user in
$this->socialite->getGuard()->login($socialiteUser->getUser(), $this->socialite->getPlugin()->getRememberLogin());

// Dispatch the login event
Events\Login::dispatch($socialiteUser);

// Redirect as intended
return redirect()->intended(
route($this->socialite->getPlugin()->getDashboardRouteName())
);
return app()->call($this->socialite->getLoginRedirectCallback(), ['provider' => $provider, 'socialiteUser' => $socialiteUser]);
}

protected function registerSocialiteUser(string $provider, SocialiteUserContract $oauthUser, Authenticatable $user): RedirectResponse
Expand All @@ -118,7 +115,7 @@ protected function registerSocialiteUser(string $provider, SocialiteUserContract
Events\SocialiteUserConnected::dispatch($socialiteUser);

// Login the user
return $this->loginUser($socialiteUser);
return $this->loginUser($provider, $socialiteUser);
}

protected function registerOauthUser(string $provider, SocialiteUserContract $oauthUser): RedirectResponse
Expand All @@ -135,7 +132,7 @@ protected function registerOauthUser(string $provider, SocialiteUserContract $oa
Events\Registered::dispatch($socialiteUser);

// Login the user
return $this->loginUser($socialiteUser);
return $this->loginUser($provider, $socialiteUser);
}

public function processCallback(string $provider): RedirectResponse
Expand All @@ -161,7 +158,7 @@ public function processCallback(string $provider): RedirectResponse
// Try to find a socialite user
$socialiteUser = $this->retrieveSocialiteUser($provider, $oauthUser);
if ($socialiteUser) {
return $this->loginUser($socialiteUser);
return $this->loginUser($provider, $socialiteUser);
}

// See if a user already exists, but not for this socialite provider
Expand Down
11 changes: 11 additions & 0 deletions tests/Fixtures/TestTeam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests\Fixtures;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;

class TestTeam extends Model
{
protected $table = 'teams';
}
36 changes: 36 additions & 0 deletions tests/Fixtures/TestTenantUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests\Fixtures;

use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Collection;

/**
* @property Collection<\DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam> $teams
*/
class TestTenantUser extends TestUser implements HasTenants
{
public function canAccessTenant(Model $tenant): bool
{
return $this->teams->contains($tenant);
}

/**
* @return \Illuminate\Support\Collection<int, \DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam>
*/
public function getTenants(Panel $panel): Collection
{
return $this->teams;
}

/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam>
*/
public function teams(): BelongsToMany
{
return $this->belongsToMany(TestTeam::class, 'team_user', 'user_id', 'team_id');
}
}
22 changes: 22 additions & 0 deletions tests/Fixtures/create_team_user_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class() extends Migration {
public function up(): void
{
Schema::create('team_user', function (Blueprint $table) {
$table->integer('team_id');
$table->integer('user_id');

$table->unique(['team_id', 'user_id']);
});
}

public function down(): void
{
Schema::dropIfExists('team_user');
}
};
20 changes: 20 additions & 0 deletions tests/Fixtures/create_teams_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class() extends Migration {
public function up(): void
{
Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->string('name');
});
}

public function down(): void
{
Schema::dropIfExists('teams');
}
};
74 changes: 74 additions & 0 deletions tests/SocialiteTenantLoginTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests;

use DutchCodingCompany\FilamentSocialite\Facades\FilamentSocialite;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use DutchCodingCompany\FilamentSocialite\Models\SocialiteUser;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestSocialiteUser;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTenantUser;
use Filament\Facades\Filament;
use Filament\Panel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Crypt;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Facades\Socialite;
use Mockery;

class SocialiteTenantLoginTest extends TestCase
{
use RefreshDatabase;

protected string $userModelClass = TestTenantUser::class;

protected array $tenantArguments = [
TestTeam::class,
];

public function testTenantLogin(): void
{
FilamentSocialite::setLoginRedirectCallback(function (string $provider, FilamentSocialiteUserContract $socialiteUser) {
assert($socialiteUser instanceof SocialiteUser);

$this->assertEquals($this->panelName, Filament::getCurrentPanel()->getId());
$this->assertEquals('github', $provider);
$this->assertEquals('github', $socialiteUser->provider);
$this->assertEquals('test-socialite-user-id', $socialiteUser->provider_id);

return redirect()->to('/some-tenant-url');
});

$response = $this
->getJson("/$this->panelName/oauth/github")
->assertStatus(302);

$state = session()->get('state');

Socialite::shouldReceive('driver')
->with('github')
->andReturn(
Mockery::mock(Provider::class)
->shouldReceive('user')
->andReturn(new TestSocialiteUser())
->getMock()
);

// Fake oauth response.
$response = $this
->getJson("/oauth/callback/github?state=$state")
->assertStatus(302);

$this->assertStringContainsString('/some-tenant-url', $response->headers->get('Location'));

$this->assertDatabaseHas('socialite_users', [
'provider' => 'github',
'provider_id' => 'test-socialite-user-id',
]);

$this->assertDatabaseHas('users', [
'name' => 'test-socialite-user-name',
'email' => '[email protected]',
]);
}
}
13 changes: 12 additions & 1 deletion tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ class TestCase extends Orchestra

protected string $panelName = 'testpanel';

/**
* @var class-string<\Illuminate\Contracts\Auth\Authenticatable>
*/
protected string $userModelClass = TestUser::class;

/**
* @var array{0: ?string, 1?: ?string, 2?: ?string}
*/
protected array $tenantArguments = [null];

protected function setUp(): void
{
parent::setUp();
Expand Down Expand Up @@ -60,6 +70,7 @@ protected function registerTestPanel(): void
->default()
->id($this->panelName)
->path($this->panelName)
->tenant(...$this->tenantArguments)
->login()
->pages([
Dashboard::class,
Expand All @@ -79,7 +90,7 @@ protected function registerTestPanel(): void
],
])
->setRegistrationEnabled(true)
->setUserModelClass(TestUser::class),
->setUserModelClass($this->userModelClass),
]),
);
}
Expand Down

0 comments on commit 5f9196d

Please sign in to comment.