From 7f4e29cba3213d935f92a7562490158739cb8811 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Sun, 5 Oct 2025 12:44:34 +0545 Subject: [PATCH] feat: implement user access controls for frontend panel --- config/eclipse-frontend.php | 15 +++ config/frontend-panel.php | 15 --- src/Http/Middleware/CheckFrontendFeatures.php | 78 +++++++++++++ src/Pages/Auth/Login.php | 41 +++++++ src/Providers/FrontendPanelProvider.php | 72 +++++------- tests/Feature/FrontendAccessSettingsTest.php | 86 ++++++++++++++ tests/Pest.php | 27 ++++- tests/TestCase.php | 9 ++ .../CheckFrontendFeaturesMiddlewareTest.php | 105 ++++++++++++++++++ workbench/app/Models/Site.php | 22 ++++ .../app/Providers/FrontendPanelProvider.php | 52 +++++++++ .../Providers/WorkbenchServiceProvider.php | 4 +- workbench/database/factories/SiteFactory.php | 19 ++++ 13 files changed, 478 insertions(+), 67 deletions(-) create mode 100644 config/eclipse-frontend.php delete mode 100644 config/frontend-panel.php create mode 100644 src/Http/Middleware/CheckFrontendFeatures.php create mode 100644 src/Pages/Auth/Login.php create mode 100644 tests/Feature/FrontendAccessSettingsTest.php create mode 100644 tests/Unit/CheckFrontendFeaturesMiddlewareTest.php create mode 100644 workbench/app/Models/Site.php create mode 100644 workbench/app/Providers/FrontendPanelProvider.php create mode 100644 workbench/database/factories/SiteFactory.php diff --git a/config/eclipse-frontend.php b/config/eclipse-frontend.php new file mode 100644 index 0000000..f2c46bf --- /dev/null +++ b/config/eclipse-frontend.php @@ -0,0 +1,15 @@ + [ + 'enabled' => false, + 'model' => null, + 'foreign_key' => null, + ], +]; diff --git a/config/frontend-panel.php b/config/frontend-panel.php deleted file mode 100644 index b5e2ce1..0000000 --- a/config/frontend-panel.php +++ /dev/null @@ -1,15 +0,0 @@ - (bool) env('FRONTEND_GUEST_ACCESS', true), -]; diff --git a/src/Http/Middleware/CheckFrontendFeatures.php b/src/Http/Middleware/CheckFrontendFeatures.php new file mode 100644 index 0000000..3e64322 --- /dev/null +++ b/src/Http/Middleware/CheckFrontendFeatures.php @@ -0,0 +1,78 @@ +route()?->getName(); + + if (! $routeName) { + return $next($request); + } + + $site = Registry::getSite(); + + if (! $site) { + return $next($request); + } + + if ($this->isAuthRoute($routeName) && ! $this->getSetting($site->id, 'enable_logins')) { + abort(404); + } + + if ($this->isRegisterRoute($routeName) && ! $this->getSetting($site->id, 'allow_registration')) { + abort(404); + } + + if (! $this->isAuthRoute($routeName) && ! $this->isRegisterRoute($routeName)) { + $guestAccess = $this->getSetting($site->id, 'guest_access'); + $enableLogins = $this->getSetting($site->id, 'enable_logins'); + + if (! $guestAccess && ! Filament::auth()->check()) { + if ($enableLogins) { + return redirect()->route('filament.frontend.auth.login'); + } + + abort(404); + } + } + + return $next($request); + } + + private function isAuthRoute(string $routeName): bool + { + return str_contains($routeName, '.auth.login') + || str_contains($routeName, '.auth.password-reset'); + } + + private function isRegisterRoute(string $routeName): bool + { + return str_contains($routeName, '.auth.register'); + } + + private function getSetting(int $siteId, string $settingName): bool + { + try { + $value = DB::table('settings') + ->where('group', 'frontend') + ->where('name', $settingName) + ->where('site_id', $siteId) + ->value('payload'); + + return $value !== null ? json_decode($value) : true; + } catch (Exception $e) { + return true; + } + } +} diff --git a/src/Pages/Auth/Login.php b/src/Pages/Auth/Login.php new file mode 100644 index 0000000..bb796ca --- /dev/null +++ b/src/Pages/Auth/Login.php @@ -0,0 +1,41 @@ +allowRegistration()) { + Filament::getCurrentPanel()?->registration(false); + } + + parent::mount(); + } + + private function allowRegistration(): bool + { + try { + $site = Registry::getSite(); + + if (! $site) { + return true; + } + + $allowRegistration = DB::table('settings') + ->where('group', 'frontend') + ->where('name', 'allow_registration') + ->where('site_id', $site->id) + ->value('payload'); + + return $allowRegistration !== null ? json_decode($allowRegistration) : true; + } catch (\Exception $e) { + return true; + } + } +} diff --git a/src/Providers/FrontendPanelProvider.php b/src/Providers/FrontendPanelProvider.php index d31089a..0928706 100644 --- a/src/Providers/FrontendPanelProvider.php +++ b/src/Providers/FrontendPanelProvider.php @@ -2,16 +2,15 @@ namespace Eclipse\Frontend\Providers; -use BezhanSalleh\FilamentShield\Middleware\SyncShieldTenant; use Eclipse\Common\Providers\GlobalSearchProvider; -use Eclipse\Core\Models\Site; use Eclipse\Core\Services\Registry; use Eclipse\Frontend\Filament\Pages as CustomPages; -use Filament\Http\Middleware\Authenticate; +use Eclipse\Frontend\Http\Middleware\CheckFrontendFeatures; +use Eclipse\Frontend\Pages\Auth\Login; +use Filament\Facades\Filament; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; -use Filament\Pages; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; @@ -43,28 +42,23 @@ public function panel(Panel $panel): Panel SubstituteBindings::class, DisableBladeIconComponents::class, DispatchServingFilamentEvent::class, + CheckFrontendFeatures::class, ]; - $widgets = []; - $pages = []; + $widgets = [ + Widgets\AccountWidget::class, + Widgets\FilamentInfoWidget::class, + ]; + + $pages = [ + CustomPages\Home::class, + ]; - if ($this->allowGuestAccess()) { - $middleware[] = AuthenticateSession::class; - $pages[] = CustomPages\Home::class; - } else { - $widgets = array_merge($widgets, [ - Widgets\AccountWidget::class, - Widgets\FilamentInfoWidget::class, - ]); - $pages[] = Pages\Dashboard::class; - } + $middleware[] = AuthenticateSession::class; $panel ->id(self::PANEL_ID) ->path('') - ->login() - ->passwordReset() - ->emailVerification() ->colors([ 'primary' => Color::Cyan, 'gray' => Color::Slate, @@ -83,33 +77,26 @@ public function panel(Panel $panel): Panel Platform::Mac => '⌘K', default => null, }) - ->tenant(Site::class, slugAttribute: 'domain') - ->tenantDomain('{tenant:domain}') - ->tenantMiddleware([ - SyncShieldTenant::class, - ], isPersistent: true) - ->tenantMenu(false) ->widgets($widgets) ->middleware($middleware) ->plugins(array_merge([ EnvironmentIndicatorPlugin::make(), ], app(Registry::class)->getPlugins())); - match ($this->allowGuestAccess()) { - true => $panel - ->renderHook( - PanelsRenderHook::TOPBAR_END, - fn () => view('frontend-panel::filament.components.theme-switcher') - ), - false => $panel - ->authMiddleware([ - Authenticate::class, - ]) - ->renderHook( - PanelsRenderHook::HEAD_START, - fn (): string => self::getThemeIsolationScript(self::PANEL_ID) - ) - }; + $panel + ->login(Login::class) + ->passwordReset() + ->registration(); + + $panel + ->renderHook( + PanelsRenderHook::TOPBAR_END, + fn () => ! Filament::auth()->check() ? view('frontend-panel::filament.components.theme-switcher') : '' + ) + ->renderHook( + PanelsRenderHook::HEAD_START, + fn (): string => self::getThemeIsolationScript(self::PANEL_ID) + ); return $panel; } @@ -133,9 +120,4 @@ private static function getThemeIsolationScript(string $panelId): string }); "; } - - private function allowGuestAccess(): bool - { - return config('frontend-panel.guest_access'); - } } diff --git a/tests/Feature/FrontendAccessSettingsTest.php b/tests/Feature/FrontendAccessSettingsTest.php new file mode 100644 index 0000000..2d3c691 --- /dev/null +++ b/tests/Feature/FrontendAccessSettingsTest.php @@ -0,0 +1,86 @@ +create(['domain' => 'site1.test']); + $site2 = $siteModel::factory()->create(['domain' => 'site2.test']); + + setSetting($site1->id, 'guest_access', false); + setSetting($site2->id, 'guest_access', true); + + $setting1 = DB::table('settings') + ->where('group', 'frontend') + ->where('name', 'guest_access') + ->where('site_id', $site1->id) + ->value('payload'); + + $setting2 = DB::table('settings') + ->where('group', 'frontend') + ->where('name', 'guest_access') + ->where('site_id', $site2->id) + ->value('payload'); + + expect(json_decode($setting1))->toBeFalse() + ->and(json_decode($setting2))->toBeTrue(); + }); + + it('allows different sites to have different access control settings', function () { + $siteModel = config('eclipse-frontend.tenancy.model'); + $site1 = $siteModel::where('domain', 'singa.lndo.site')->first(); + $site2 = $siteModel::factory()->create(['domain' => 'site2.test']); + + setSetting($site1->id, 'enable_logins', true); + setSetting($site1->id, 'allow_registration', false); + + setSetting($site2->id, 'enable_logins', false); + setSetting($site2->id, 'allow_registration', true); + + $logins1 = json_decode(DB::table('settings') + ->where('site_id', $site1->id) + ->where('name', 'enable_logins') + ->value('payload')); + + $registration1 = json_decode(DB::table('settings') + ->where('site_id', $site1->id) + ->where('name', 'allow_registration') + ->value('payload')); + + $logins2 = json_decode(DB::table('settings') + ->where('site_id', $site2->id) + ->where('name', 'enable_logins') + ->value('payload')); + + $registration2 = json_decode(DB::table('settings') + ->where('site_id', $site2->id) + ->where('name', 'allow_registration') + ->value('payload')); + + expect($logins1)->toBeTrue() + ->and($registration1)->toBeFalse() + ->and($logins2)->toBeFalse() + ->and($registration2)->toBeTrue(); + }); + + it('updates existing settings', function () { + $siteModel = config('eclipse-frontend.tenancy.model'); + $site = $siteModel::where('domain', 'singa.lndo.site')->first(); + + setSetting($site->id, 'guest_access', true); + $first = json_decode(DB::table('settings') + ->where('site_id', $site->id) + ->where('name', 'guest_access') + ->value('payload')); + + setSetting($site->id, 'guest_access', false); + $second = json_decode(DB::table('settings') + ->where('site_id', $site->id) + ->where('name', 'guest_access') + ->value('payload')); + + expect($first)->toBeTrue() + ->and($second)->toBeFalse(); + }); +}); diff --git a/tests/Pest.php b/tests/Pest.php index a5fd441..1ea249e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,6 +1,5 @@ use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->beforeEach(function () { - $this->seed(CoreSeeder::class); + $siteModel = config('eclipse-frontend.tenancy.model'); + $site = $siteModel::firstOrCreate( + ['domain' => 'singa.lndo.site'], + ['name' => 'Test Site'] + ); + + \Eclipse\Core\Services\Registry::setSite($site->id); }) ->in(__DIR__); @@ -47,7 +52,17 @@ | */ -// function something() -// { -// // .. -// } +function setSetting(int $siteId, string $name, bool $value): void +{ + DB::table('settings')->updateOrInsert( + [ + 'group' => 'frontend', + 'name' => $name, + 'site_id' => $siteId, + ], + [ + 'locked' => false, + 'payload' => json_encode($value), + ] + ); +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 605ebf2..895cc2b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,6 +26,15 @@ protected function setUp(): void $this->withoutVite(); + config(['eclipse-frontend.tenancy.enabled' => false]); + config(['eclipse-frontend.tenancy.model' => 'Workbench\\App\\Models\\Site']); + config(['eclipse-frontend.tenancy.foreign_key' => 'site_id']); + + config(['auth.guards.frontend' => [ + 'driver' => 'session', + 'provider' => 'users', + ]]); + // Ensure SetupSite middleware is applied during tests // This is done here since the "withMiddleware" method in workbench/bootstrap/app.php does not seem to work // $this->withMiddleware(SetupTenant::class) also does not work diff --git a/tests/Unit/CheckFrontendFeaturesMiddlewareTest.php b/tests/Unit/CheckFrontendFeaturesMiddlewareTest.php new file mode 100644 index 0000000..62b4995 --- /dev/null +++ b/tests/Unit/CheckFrontendFeaturesMiddlewareTest.php @@ -0,0 +1,105 @@ +site = $siteModel::firstOrCreate( + ['domain' => 'singa.lndo.site'], + ['name' => 'Test Site'] + ); + + $this->middleware = new CheckFrontendFeatures; + + Route::get('/test', fn () => 'ok')->name('filament.frontend.pages.home'); + Route::get('/login', fn () => 'login')->name('filament.frontend.auth.login'); + Route::get('/register', fn () => 'register')->name('filament.frontend.auth.register'); +}); + +describe('Middleware Logic', function () { + it('allows request when site context is not available', function () { + $request = Request::create('/test'); + $request->setRouteResolver(fn () => Route::getRoutes()->match($request)); + + $response = $this->middleware->handle($request, fn ($req) => response('passed')); + + expect($response->getContent())->toBe('passed'); + }); + + it('blocks login route when enable_logins is false', function () { + Registry::setSite($this->site->id); + setSetting($this->site->id, 'enable_logins', false); + + $request = Request::create('/login'); + $request->setRouteResolver(fn () => Route::getRoutes()->match($request)); + + $this->expectException(NotFoundHttpException::class); + + $this->middleware->handle($request, fn ($req) => response('should not reach')); + }); + + it('allows login route when enable_logins is true', function () { + Registry::setSite($this->site->id); + setSetting($this->site->id, 'enable_logins', true); + + $request = Request::create('/login'); + $request->setRouteResolver(fn () => Route::getRoutes()->match($request)); + + $response = $this->middleware->handle($request, fn ($req) => response('passed')); + + expect($response->getContent())->toBe('passed'); + }); + + it('blocks registration route when allow_registration is false', function () { + Registry::setSite($this->site->id); + setSetting($this->site->id, 'allow_registration', false); + + $request = Request::create('/register'); + $request->setRouteResolver(fn () => Route::getRoutes()->match($request)); + + $this->expectException(NotFoundHttpException::class); + + $this->middleware->handle($request, fn ($req) => response('should not reach')); + }); + + it('allows registration route when allow_registration is true', function () { + Registry::setSite($this->site->id); + setSetting($this->site->id, 'allow_registration', true); + + $request = Request::create('/register'); + $request->setRouteResolver(fn () => Route::getRoutes()->match($request)); + + $response = $this->middleware->handle($request, fn ($req) => response('passed')); + + expect($response->getContent())->toBe('passed'); + }); + + it('allows guest access when guest_access is true', function () { + Registry::setSite($this->site->id); + setSetting($this->site->id, 'guest_access', true); + + $request = Request::create('/test'); + $request->setRouteResolver(fn () => Route::getRoutes()->match($request)); + + $response = $this->middleware->handle($request, fn ($req) => response('passed')); + + expect($response->getContent())->toBe('passed'); + }); + + it('blocks guest access when guest_access is false and user not authenticated', function () { + Registry::setSite($this->site->id); + setSetting($this->site->id, 'guest_access', false); + setSetting($this->site->id, 'enable_logins', false); + + $request = Request::create('/test'); + $request->setRouteResolver(fn () => Route::getRoutes()->match($request)); + + $this->expectException(NotFoundHttpException::class); + + $this->middleware->handle($request, fn ($req) => response('should not reach')); + }); +}); diff --git a/workbench/app/Models/Site.php b/workbench/app/Models/Site.php new file mode 100644 index 0000000..af921a9 --- /dev/null +++ b/workbench/app/Models/Site.php @@ -0,0 +1,22 @@ +id('frontend') + ->path('') + ->colors([ + 'primary' => Color::Cyan, + 'gray' => Color::Slate, + ]) + ->authGuard('frontend') + ->login(Login::class) + ->passwordReset() + ->registration() + ->pages([ + Home::class, + ]) + ->middleware([ + EncryptCookies::class, + AddQueuedCookiesToResponse::class, + StartSession::class, + ShareErrorsFromSession::class, + VerifyCsrfToken::class, + SubstituteBindings::class, + DisableBladeIconComponents::class, + DispatchServingFilamentEvent::class, + CheckFrontendFeatures::class, + AuthenticateSession::class, + ]); + } +} diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php index 5c21824..288aa4e 100644 --- a/workbench/app/Providers/WorkbenchServiceProvider.php +++ b/workbench/app/Providers/WorkbenchServiceProvider.php @@ -2,6 +2,7 @@ namespace Workbench\App\Providers; +use Eclipse\Frontend\FrontendServiceProvider as PackageFrontendServiceProvider; use Illuminate\Support\ServiceProvider; class WorkbenchServiceProvider extends ServiceProvider @@ -11,7 +12,8 @@ class WorkbenchServiceProvider extends ServiceProvider */ public function register(): void { - $this->app->register(AdminPanelProvider::class); + $this->app->register(PackageFrontendServiceProvider::class); + $this->app->register(FrontendPanelProvider::class); } /** diff --git a/workbench/database/factories/SiteFactory.php b/workbench/database/factories/SiteFactory.php new file mode 100644 index 0000000..2de64b5 --- /dev/null +++ b/workbench/database/factories/SiteFactory.php @@ -0,0 +1,19 @@ + fake()->company(), + 'domain' => fake()->domainName(), + ]; + } +}