-
Notifications
You must be signed in to change notification settings - Fork 29
feat: admin panel module with tenant-aware infrastructure #206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
0b30bdb
feat(panel-admin): scaffold module structure
danielhe4rt c26124c
feat(panel-admin): add Dashboard page
danielhe4rt 2ffcb47
feat(panel-admin): add ApplyTenantScopes middleware with tests
danielhe4rt e0c813e
feat(panel-admin): add config and ServiceProvider
danielhe4rt 1ae28fe
fix(panel-admin): use BackedEnum type for Dashboard navigationIcon
danielhe4rt 5f84d83
feat(panel-admin): add AdminPanelProvider and register in bootstrap
danielhe4rt fae36de
test(panel-admin): add admin panel access control tests
danielhe4rt 47fc204
feat(panel-admin): add authMiddleware and access control tests
danielhe4rt ad042e1
style: fix pint formatting in bootstrap/providers.php
danielhe4rt 7521313
refactor(seeders): remove ThreeDotsSeeder and create super admin
danielhe4rt c1ce8a1
fix: migration to delete tenant_id=2 and all dependent records
danielhe4rt c3fe517
chore(panel-admin): remove base css
danielhe4rt 0a8f20f
fix: add phpstan ignore for Panel macro and guard migration to produc…
danielhe4rt 76dc656
wip: composer.lock
danielhe4rt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "name": "he4rt/panel-admin", | ||
| "description": "", | ||
| "type": "library", | ||
| "version": "1.0", | ||
| "license": "proprietary", | ||
| "require": {}, | ||
| "autoload": { | ||
| "psr-4": { | ||
| "He4rt\\PanelAdmin\\": "src/", | ||
| "He4rt\\PanelAdmin\\Database\\Factories\\": "database/factories/", | ||
| "He4rt\\PanelAdmin\\Database\\Seeders\\": "database/seeders/" | ||
| } | ||
| }, | ||
| "autoload-dev": { | ||
| "psr-4": { | ||
| "He4rt\\PanelAdmin\\Tests\\": "tests/" | ||
| } | ||
| }, | ||
| "minimum-stability": "stable", | ||
| "extra": { | ||
| "laravel": { | ||
| "providers": [ | ||
| "He4rt\\PanelAdmin\\PanelAdminServiceProvider" | ||
| ] | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| return [ | ||
| 'modules' => [ | ||
| // Modules whose Filament/Admin/ resources will be discovered | ||
| // Ex: 'identity', 'gamification', 'events' | ||
| ], | ||
|
|
||
| 'tenant_scoped_models' => [ | ||
| // Models that receive tenant global scope | ||
| // Ex: \He4rt\Identity\User\Models\User::class, | ||
| ], | ||
| ]; |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| parameters: | ||
| ignoreErrors: [] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| includes: | ||
| - phpstan.ignore.neon | ||
|
|
||
| parameters: | ||
| paths: | ||
| - src/ |
Empty file.
32 changes: 32 additions & 0 deletions
32
app-modules/panel-admin/src/Http/Middleware/ApplyTenantScopes.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\PanelAdmin\Http\Middleware; | ||
|
|
||
| use Closure; | ||
| use Filament\Facades\Filament; | ||
| use Illuminate\Database\Eloquent\Builder; | ||
| use Illuminate\Http\Request; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
|
|
||
| class ApplyTenantScopes | ||
| { | ||
| public function handle(Request $request, Closure $next): Response | ||
| { | ||
| $tenant = Filament::getTenant(); | ||
|
|
||
| if (!$tenant) { | ||
| return $next($request); | ||
| } | ||
|
|
||
| foreach (config('panel-admin.tenant_scoped_models', []) as $model) { | ||
| $model::addGlobalScope( | ||
| 'tenant', | ||
| fn (Builder $query) => $query->whereBelongsTo($tenant), | ||
| ); | ||
| } | ||
|
|
||
| return $next($request); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\PanelAdmin\Pages; | ||
|
|
||
| use BackedEnum; | ||
| use Filament\Pages\Dashboard as BaseDashboard; | ||
|
|
||
| class Dashboard extends BaseDashboard | ||
| { | ||
| protected static string|null|BackedEnum $navigationIcon = 'heroicon-o-home'; | ||
|
|
||
| protected static ?string $title = 'Dashboard'; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\PanelAdmin; | ||
|
|
||
| use Illuminate\Support\ServiceProvider; | ||
|
|
||
| class PanelAdminServiceProvider extends ServiceProvider | ||
| { | ||
| public function register(): void | ||
| { | ||
| $this->mergeConfigFrom(__DIR__.'/../config/panel-admin.php', 'panel-admin'); | ||
| } | ||
|
|
||
| public function boot(): void | ||
| { | ||
| $this->loadViewsFrom(__DIR__.'/../resources/views', 'panel-admin'); | ||
| } | ||
| } |
Empty file.
57 changes: 57 additions & 0 deletions
57
app-modules/panel-admin/tests/Feature/AdminPanelAccessTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Filament\Facades\Filament; | ||
| use He4rt\Identity\Tenant\Models\Tenant; | ||
| use He4rt\Identity\User\Models\User; | ||
|
|
||
| test('unauthenticated user is redirected to login', function (): void { | ||
| $tenant = Tenant::factory()->create(['slug' => 'he4rt-dev']); | ||
|
|
||
| $this | ||
| ->get('/admin/'.$tenant->slug) | ||
| ->assertRedirect('/admin/login'); | ||
| }); | ||
|
|
||
| test('admin login page renders', function (): void { | ||
| $this | ||
| ->get('/admin/login') | ||
| ->assertOk(); | ||
| }); | ||
|
|
||
| test('authenticated admin can access admin panel', function (): void { | ||
| $tenant = Tenant::factory()->create(['slug' => 'he4rt-dev']); | ||
| $user = User::factory()->create(['username' => 'danielhe4rt']); | ||
|
|
||
| $tenant->members()->attach($user); | ||
|
|
||
| config(['he4rt.admins' => 'danielhe4rt']); | ||
|
|
||
| $this | ||
| ->actingAs($user) | ||
| ->get('/admin') | ||
| ->assertRedirect(); | ||
| }); | ||
|
|
||
| test('admin user can access panel via canAccessPanel', function (): void { | ||
| $user = User::factory()->create(['username' => 'danielhe4rt']); | ||
|
|
||
| config(['he4rt.admins' => 'danielhe4rt']); | ||
|
|
||
| $panel = Filament::getPanel('admin'); | ||
|
|
||
| expect($user->canAccessPanel($panel))->toBeTrue(); | ||
| }); | ||
|
|
||
| test('non-admin user cannot access admin panel in production', function (): void { | ||
| $user = User::factory()->create(['username' => 'regular-user']); | ||
|
|
||
| config(['he4rt.admins' => 'danielhe4rt']); | ||
|
|
||
| app()->detectEnvironment(fn () => 'production'); | ||
|
|
||
| $panel = Filament::getPanel('admin'); | ||
|
|
||
| expect($user->canAccessPanel($panel))->toBeFalse(); | ||
| }); | ||
|
danielhe4rt marked this conversation as resolved.
|
||
71 changes: 71 additions & 0 deletions
71
app-modules/panel-admin/tests/Feature/ApplyTenantScopesTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Filament\Facades\Filament; | ||
| use He4rt\Events\Models\EventModel; | ||
| use He4rt\Identity\Tenant\Models\Tenant; | ||
| use He4rt\Identity\User\Models\User; | ||
| use He4rt\PanelAdmin\Http\Middleware\ApplyTenantScopes; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Http\Response; | ||
|
|
||
| test('applies global scope to configured models when tenant is set', function (): void { | ||
| $tenant = Tenant::factory()->create(); | ||
|
|
||
| Filament::shouldReceive('getTenant') | ||
| ->andReturn($tenant); | ||
|
|
||
| config(['panel-admin.tenant_scoped_models' => [ | ||
| EventModel::class, | ||
| ]]); | ||
|
|
||
| $middleware = new ApplyTenantScopes(); | ||
|
|
||
| $middleware->handle( | ||
| Request::create('/admin'), | ||
| fn () => new Response(), | ||
| ); | ||
|
|
||
| $query = EventModel::query()->toSql(); | ||
|
|
||
| expect($query)->toContain('where'); | ||
| }); | ||
|
danielhe4rt marked this conversation as resolved.
|
||
|
|
||
| test('skips scope application when no tenant is set', function (): void { | ||
| Filament::shouldReceive('getTenant') | ||
| ->andReturn(null); | ||
|
|
||
| config(['panel-admin.tenant_scoped_models' => [ | ||
| User::class, | ||
| ]]); | ||
|
|
||
| $middleware = new ApplyTenantScopes(); | ||
|
|
||
| $middleware->handle( | ||
| Request::create('/admin'), | ||
| fn () => new Response(), | ||
| ); | ||
|
|
||
| $query = User::query()->toSql(); | ||
|
|
||
| expect($query)->not->toContain('tenant_id'); | ||
| }); | ||
|
|
||
| test('handles empty tenant_scoped_models config', function (): void { | ||
| $tenant = Tenant::factory()->create(); | ||
|
|
||
| Filament::shouldReceive('getTenant') | ||
| ->andReturn($tenant); | ||
|
|
||
| config(['panel-admin.tenant_scoped_models' => []]); | ||
|
|
||
| $middleware = new ApplyTenantScopes(); | ||
|
|
||
| $response = $middleware->handle( | ||
| Request::create('/admin'), | ||
| fn () => new Response('ok'), | ||
| ); | ||
|
|
||
| expect($response->getContent())->toBe('ok'); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Providers\Filament; | ||
|
|
||
| use App\Enums\FilamentPanel; | ||
| use App\Filament\Pages\Login; | ||
| use Filament\Http\Middleware\Authenticate; | ||
| use Filament\Http\Middleware\AuthenticateSession; | ||
| use Filament\Http\Middleware\DisableBladeIconComponents; | ||
| use Filament\Http\Middleware\DispatchServingFilamentEvent; | ||
| use Filament\Panel; | ||
| use Filament\PanelProvider; | ||
| use Filament\Support\Colors\Color; | ||
| use He4rt\Identity\Tenant\Models\Tenant; | ||
| use He4rt\PanelAdmin\Pages\Dashboard; | ||
| use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; | ||
| use Illuminate\Cookie\Middleware\EncryptCookies; | ||
| use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; | ||
| use Illuminate\Routing\Middleware\SubstituteBindings; | ||
| use Illuminate\Session\Middleware\StartSession; | ||
| use Illuminate\View\Middleware\ShareErrorsFromSession; | ||
|
|
||
| class AdminPanelProvider extends PanelProvider | ||
| { | ||
| public function panel(Panel $panel): Panel | ||
| { | ||
| $panel | ||
| ->id('admin') | ||
| ->path('admin') | ||
| ->login(Login::class) | ||
| ->colors([ | ||
| 'primary' => Color::Purple, | ||
| ]) | ||
| ->tenant(Tenant::class, slugAttribute: 'slug') | ||
| ->middleware([ | ||
| EncryptCookies::class, | ||
| AddQueuedCookiesToResponse::class, | ||
| StartSession::class, | ||
| AuthenticateSession::class, | ||
| ShareErrorsFromSession::class, | ||
| VerifyCsrfToken::class, | ||
| SubstituteBindings::class, | ||
| DisableBladeIconComponents::class, | ||
| DispatchServingFilamentEvent::class, | ||
| ]) | ||
| ->pages([ | ||
| Dashboard::class, | ||
| ]) | ||
| ->authMiddleware([ | ||
| Authenticate::class, | ||
| ]); | ||
|
danielhe4rt marked this conversation as resolved.
|
||
|
|
||
| foreach (config('panel-admin.modules', []) as $module) { | ||
| $panel->discoverResourcesForPanel($module, FilamentPanel::Admin); | ||
| } | ||
|
|
||
| return $panel; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.