From 631143f1607fe10ce8e8191c12d99df139de757c Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Thu, 2 Oct 2025 19:48:45 +0530 Subject: [PATCH] feat: add PanelSetting to the `dash` panel --- app/Filament/Pages/PanelSettings.php | 288 ++++++++++++++++++ app/Helper/ArrayHelper.php | 14 + app/Helper/ColorHelper.php | 156 ++++++++++ app/Models/User.php | 52 +++- app/Providers/Filament/DashPanelProvider.php | 174 +++++++++-- ...059_add_app_auth_fields_to_users_table.php | 34 +++ .../filament/pages/panel-settings.blade.php | 8 + 7 files changed, 697 insertions(+), 29 deletions(-) create mode 100644 app/Filament/Pages/PanelSettings.php create mode 100644 app/Helper/ArrayHelper.php create mode 100644 app/Helper/ColorHelper.php create mode 100644 database/migrations/2025_10_01_000059_add_app_auth_fields_to_users_table.php create mode 100644 resources/views/filament/pages/panel-settings.blade.php diff --git a/app/Filament/Pages/PanelSettings.php b/app/Filament/Pages/PanelSettings.php new file mode 100644 index 0000000..0e8be65 --- /dev/null +++ b/app/Filament/Pages/PanelSettings.php @@ -0,0 +1,288 @@ + | null + */ + public ?array $data = []; + + protected static ?string $title = 'Panel'; + + protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-group'; // Uncomment if you want to set a custom navigation icon + + // protected ?string $subheading = ''; // Uncomment if you want to set a custom subheading + + // protected static ?string $slug = 'panel-settings'; // Uncomment if you want to set a custom slug + + protected string $view = 'filament.pages.panel-settings'; + + protected function settingName(): string + { + return 'panel'; + } + + /** + * Provide default values. + * + */ + + protected function getPanelID(): string + { + return Filament::getCurrentOrDefaultPanel()->getId(); + } + + public function getDefaultData(): array + { + return [ + 'panel_id' => $this->getPanelID(), + 'panel_route_'.$this->getPanelID() => $this->getPanelID(), + 'panel_show_logo_'.$this->getPanelID() => 'hide', + 'panel_spa_'.$this->getPanelID() => false, + 'panel_spa_prefetch_'.$this->getPanelID() => false, + 'panel_spa_exceptions_'.$this->getPanelID() => [], + 'panel_enable_login_'.$this->getPanelID() => true, + 'panel_font_'.$this->getPanelID() => 'Poppins', + 'panel_color_isRGB_'.$this->getPanelID() => false, + 'panel_enable_mfa_'.$this->getPanelID() => false, + 'panel_mfa_type_'.$this->getPanelID() => 'app', + 'panel_mfa_app_recoverable_'.$this->getPanelID() => false, + 'panel_mfa_app_recovery_code_regeneratable_'.$this->getPanelID() => false, + 'panel_mfa_app_code_window_'.$this->getPanelID() => 4, + 'panel_mfa_app_recovery_code_count_'.$this->getPanelID() => 8, + 'panel_mfa_email_code_expiry_'.$this->getPanelID() => 2, + 'panel_mfa_is_required_'.$this->getPanelID() => 'no', + + ]; + } + + public function form(Schema $schema): Schema + { + return $schema + ->components([ + Components\Section::make('Panel Branding')->schema([ + TextInput::make('panel_id') + ->label('Panel ID') + ->disabled(), + TextInput::make('panel_name_'.$this->getPanelID()) + ->label('Panel Name') + ->required(), + TextInput::make('panel_route_'.$this->getPanelID()) + ->label('Panel Route') + ->required(), + Select::make('panel_show_logo_'.$this->getPanelID()) + ->label('Panel Show Logo') + ->native(false) + ->options([ + 'show' => 'Show Logo', + 'hide' => 'Hide Logo' + ]) + ->live() + ->required(), + FileUpload::make('panel_logo_light_'.$this->getPanelID()) + ->label('Panel Logo') + ->visible(fn($get) => $get('panel_show_logo_'.$this->getPanelID()) === 'show') + ->required(fn($get) => $get('panel_show_logo_'.$this->getPanelID()) === 'show') + ->image() + ->disk('public') + ->visibility('public') + ->directory('panel-assets') + ->columnSpan(1) + ->helperText('Upload a logo for panel'), + + FileUpload::make('panel_logo_dark_'.$this->getPanelID()) + ->label('Panel Dark Mode Logo') + ->visible(fn($get) => $get('panel_show_logo_'.$this->getPanelID()) === 'show') + ->required(fn($get) => $get('panel_show_logo_'.$this->getPanelID()) === 'show') + ->image() + ->disk('public') + ->visibility('public') + ->directory('panel-assets') + ->columnSpan(1) + ->helperText('Upload a logo for dark mode panel'), + + TextInput::make('panel_logo_height_'.$this->getPanelID()) + ->label('Panel Logo Height') + ->helperText('Use value like `30px`, `2rem`, `20%`, etc. This value applies as style attribute - height: 30px;') + ->columnSpanFull() + ->visible(fn($get) => $get('panel_show_logo_'.$this->getPanelID()) === 'show') + ->required(fn($get) => $get('panel_show_logo_'.$this->getPanelID()) === 'show'), + + FileUpload::make('panel_favicon_'.$this->getPanelID()) + ->label('Panel Favicon') + ->image() + ->disk('public') + ->visibility('public') + ->directory('panel-assets') + ->columnSpanFull() + ->helperText('Upload a favicon for panel'), + + TextInput::make('panel_font_' . $this->getPanelID()) + ->label('Panel Font') + ->placeholder('e.g. Inter, Poppins, or custom font name') + ->datalist([ + 'Barlow' => 'Barlow', + 'DM Sans' => 'DM Sans', + 'Hind' => 'Hind', + 'IBM Plex Sans' => 'IBM Plex Sans', + 'Inter' => 'Inter', + 'Lato' => 'Lato', + 'Manrope' => 'Manrope', + 'Mulish' => 'Mulish', + 'Nunito' => 'Nunito', + 'Open Sans' => 'Open Sans', + 'Poppins' => 'Poppins', + 'Public Sans' => 'Public Sans', + 'Quicksand' => 'Quicksand', + 'Roboto' => 'Roboto', + 'Rubik' => 'Rubik', + 'Source Sans Pro' => 'Source Sans Pro', + 'Work Sans' => 'Work Sans', + ] + ) + ->columnSpanFull() + ->helperText('Enter a Google Font name or type a custom one'), + + Repeater::make('panel_color_'.$this->getPanelID()) + ->label('Panel Color') + ->schema([ + TextInput::make('panel_color_name_'.$this->getPanelID()) + ->label('Panel Color Name') + ->required(), + ColorPicker::make('panel_color_'.$this->getPanelID()) + ->label('Pick Panel Color') + ->rgb() + ->regex('/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/') + ->live() + ->afterStateUpdated(function ($state, callable $set) { + $shades = ColorHelper::generateOklchFromRGBShades($state); + $set('panel_color_shade_' . $this->getPanelID(), $shades ?? '// Invalid RGB'); + }) + ->required(), + Textarea::make('panel_color_shade_'.$this->getPanelID()) + ->label('Panel Color Shade') + ->rows(3) + ->placeholder('Choose a color to generate shade') + ->columnSpanFull() + ])->columns(2)->columnSpanFull(), + + Toggle::make('panel_color_isRGB_'.$this->getPanelID()) + ->label('Use Panel Color as RGB') + ->helperText('If toggled `OFF` Oklch Color Shades will be used, else Only RGB will be used'), + + Toggle::make('panel_default') + ->label('Is this default panel?') + ->helperText('Toggle `ON` to set this panel as default') + ->dehydrateStateUsing(function (bool $state, $get) { + return $state ? $get('panel_id') : null; + }) + + + ])->columns(2), + + Components\Section::make('Panel Navigation')->schema([ + + Toggle::make('panel_spa_'.$this->getPanelID()) + ->label('Enable Panel SPA') + ->live(), + + Toggle::make('panel_spa_prefetch_'.$this->getPanelID()) + ->label('Panel Spa Prefetching') + ->visible(fn($get) => $get('panel_spa_'.$this->getPanelID())), + + TagsInput::make('panel_spa_exceptions_'.$this->getPanelID()) + ->label('Panel Spa Exceptions') + ->visible(fn($get) => $get('panel_spa_'.$this->getPanelID())) + ->columnSpanFull(), + ])->columns(2), + + Components\Section::make('Panel Authentication')->schema([ + Toggle::make('panel_enable_login_'.$this->getPanelID()) + ->label('Enable Login'), + Toggle::make('panel_enable_registration_'.$this->getPanelID()) + ->label('Enable Registration'), + Toggle::make('panel_enable_profile_'.$this->getPanelID()) + ->live() + ->label('Enable Profile'), + Toggle::make('panel_enable_mfa_'.$this->getPanelID()) + ->visible(fn($get) => $get('panel_enable_profile_'.$this->getPanelID())) + ->live() + ->label('Enable MFA'), + Select::make('panel_mfa_type_'.$this->getPanelID()) + ->label('MFA Type') + ->visible(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID())) + ->options([ + 'app' => 'Authenticator App', + 'email' => 'Email', + ]) + ->live() + ->required(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID())), + TextInput::make('panel_mfa_app_brand_name_'.$this->getPanelID()) + ->label('MFA Authenticator Brand Name') + ->helperText('Optional: Leave to use app name as authenticator brand name') + ->visible(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID()) && $get('panel_mfa_type_'.$this->getPanelID()) === 'app'), + + Toggle::make('panel_mfa_app_recoverable_'.$this->getPanelID()) + ->label('MFA Authenticator Recoverable') + ->live() + ->visible(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID()) && $get('panel_mfa_type_'.$this->getPanelID()) === 'app'), + + Toggle::make('panel_mfa_app_recovery_code_regeneratable_'.$this->getPanelID()) + ->label('Allow to regenerate recovery code') + ->live() + ->visible(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID()) && $get('panel_mfa_app_recoverable_'.$this->getPanelID()) && $get('panel_mfa_type_'.$this->getPanelID()) === 'app'), + + TextInput::make('panel_mfa_app_recovery_code_count_'.$this->getPanelID()) + ->label('MFA Authenticator Recovery Code Count') + ->numeric() + ->minValue(8) + ->maxValue(16) + ->visible(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID()) && $get('panel_mfa_app_recoverable_'.$this->getPanelID()) && $get('panel_mfa_type_'.$this->getPanelID()) === 'app'), + + TextInput::make('panel_mfa_app_code_window_'.$this->getPanelID()) + ->label('MFA Authenticator Code Window') + ->numeric() + ->minValue(1) + ->maxValue(8) + ->visible(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID()) && $get('panel_mfa_type_'.$this->getPanelID()) === 'app'), + + TextInput::make('panel_mfa_email_code_expiry_'.$this->getPanelID()) + ->label('MFA Email Expiry (in minutes)') + ->numeric() + ->minValue(1) + ->maxValue(8) + ->visible(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID()) && $get('panel_mfa_type_'.$this->getPanelID()) === 'email'), + + Select::make('panel_mfa_is_required_'.$this->getPanelID()) + ->label('MFA Is Required') + ->options([ + 'no' => 'No', + 'yes' => 'Yes', + ]) + ->columnSpanFull() + ->visible(fn($get) => $get('panel_enable_mfa_'.$this->getPanelID())) + ->helperText('Force to enable MFA, even for those panels in which MFA is not set yet!') + + ])->columns(2) + ]) + ->statePath('data'); + } +} diff --git a/app/Helper/ArrayHelper.php b/app/Helper/ArrayHelper.php new file mode 100644 index 0000000..46d6bfc --- /dev/null +++ b/app/Helper/ArrayHelper.php @@ -0,0 +1,14 @@ + $L, 'a' => $a, 'b' => $b]; + } + + /** + * Convert OKLab to OKLCH color space. + */ + public static function oklabToOklch($L, $a, $b) + { + $C = sqrt($a * $a + $b * $b); // Chroma + $h = atan2($b, $a); // Hue in radians + + // Convert hue to degrees and ensure it's in [0, 360) + $H = rad2deg($h); + if ($H < 0) { + $H += 360; + } + + return ['L' => $L, 'C' => $C, 'H' => $H]; + } + + /** + * Convert RGB to OKLCH color space. + */ + public static function rgbToOklch($r, $g, $b) + { + $oklab = self::rgbToOklab($r, $g, $b); + return self::oklabToOklch($oklab['L'], $oklab['a'], $oklab['b']); + } + + /** + * Mix RGB color with white or black. + */ + public static function mixRgb(int $r, int $g, int $b, float $t, bool $toWhite): array + { + $target = $toWhite ? 255 : 0; + + $rMix = (int) ($r + ($target - $r) * $t); + $gMix = (int) ($g + ($target - $g) * $t); + $bMix = (int) ($b + ($target - $b) * $t); + + return [ + max(0, min(255, $rMix)), + max(0, min(255, $gMix)), + max(0, min(255, $bMix)), + ]; + } + + /** + * Generate OKLCH shades from RGB string. + */ + public static function generateOklchFromRGBShades(string $rgbString): ?string + { + if (!preg_match('/rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/', $rgbString, $matches)) { + return null; + } + + [$_, $r, $g, $b] = $matches; + $r = (int)$r; + $g = (int)$g; + $b = (int)$b; + + $shades = [ + 50 => 0.95, + 100 => 0.9, + 200 => 0.75, + 300 => 0.6, + 400 => 0.3, + 500 => 0.0, + 600 => 0.1, + 700 => 0.25, + 800 => 0.45, + 900 => 0.65, + 950 => 0.85, + ]; + + $result = []; + + foreach ($shades as $key => $t) { + $toWhite = $key <= 500; + [$rAdj, $gAdj, $bAdj] = self::mixRgb($r, $g, $b, $t, $toWhite); + $oklch = self::rgbToOklch($rAdj, $gAdj, $bAdj); + + $result[$key] = sprintf( + 'oklch(%.3f %.3f %.3f)', + $oklch['L'], + $oklch['C'], + $oklch['H'] + ); + } + + return json_encode($result); + } + + /** + * Get panel colors from database configuration. + */ + public static function getPanelColor(string $panelId): array + { + try { + $panelColor = []; + if ($panelId != "") { + $colors = db_config('panel.panel_color_'.$panelId) ?? []; + $isRGB = db_config('panel.panel_color_isRGB_'.$panelId) ?? false; + foreach ($colors as $color) { + $colorName = $color['panel_color_name_'.$panelId]; + $colorRGB = $color['panel_color_'.$panelId]; + $colorOKLCH = json_decode($color['panel_color_shade_'.$panelId], true); + $panelColor[$colorName] = $isRGB ? $colorRGB : $colorOKLCH; + } + } + return $panelColor; + } catch (\Exception $e) { + Log::error($e->getMessage()); + return []; + } + + } +} diff --git a/app/Models/User.php b/app/Models/User.php index ad0e3e7..cbe21b6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,20 +2,23 @@ namespace App\Models; +use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthenticationRecovery; +use Filament\Auth\MultiFactor\Email\Contracts\HasEmailAuthentication; use Filament\Models\Contracts\FilamentUser; use Filament\Panel; -use Illuminate\Auth\MustVerifyEmail; +use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; use Laravel\Fortify\TwoFactorAuthenticatable; use Spatie\Permission\Traits\HasRoles; +use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication; -class User extends Authenticatable implements FilamentUser +class User extends Authenticatable implements FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, HasEmailAuthentication, MustVerifyEmail { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, TwoFactorAuthenticatable, MustVerifyEmail, HasRoles; + use HasFactory, Notifiable, TwoFactorAuthenticatable, HasRoles; /** * The attributes that are mass assignable. @@ -37,6 +40,8 @@ class User extends Authenticatable implements FilamentUser protected $hidden = [ 'password', 'remember_token', + 'app_authentication_secret', + 'app_authentication_recovery_codes', ]; /** @@ -49,6 +54,9 @@ class User extends Authenticatable implements FilamentUser return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'app_authentication_secret' => 'encrypted', + 'app_authentication_recovery_codes' => 'encrypted:array', + 'has_email_authentication' => 'boolean', ]; } @@ -68,4 +76,42 @@ class User extends Authenticatable implements FilamentUser { return $this->hasPermissionTo('manage panels'); } + + public function getAppAuthenticationSecret(): ?string + { + return $this->app_authentication_secret; + } + + public function saveAppAuthenticationSecret(?string $secret): void + { + $this->app_authentication_secret = $secret; + $this->save(); + } + + public function getAppAuthenticationHolderName(): string + { + return $this->email; + } + + public function getAppAuthenticationRecoveryCodes(): ?array + { + return $this->app_authentication_recovery_codes; + } + + public function saveAppAuthenticationRecoveryCodes(?array $codes): void + { + $this->app_authentication_recovery_codes = $codes; + $this->save(); + } + + public function hasEmailAuthentication(): bool + { + return $this->has_email_authentication; + } + + public function toggleEmailAuthentication(bool $condition): void + { + $this->has_email_authentication = $condition; + $this->save(); + } } diff --git a/app/Providers/Filament/DashPanelProvider.php b/app/Providers/Filament/DashPanelProvider.php index ae70728..c88e4b5 100644 --- a/app/Providers/Filament/DashPanelProvider.php +++ b/app/Providers/Filament/DashPanelProvider.php @@ -2,10 +2,14 @@ namespace App\Providers\Filament; +use App\Helper\ArrayHelper; +use App\Helper\ColorHelper; use Auth; use Backstage\FilamentMails\Facades\FilamentMails; use Backstage\FilamentMails\FilamentMailsPlugin; use Boquizo\FilamentLogViewer\FilamentLogViewerPlugin; +use Filament\Auth\MultiFactor\App\AppAuthentication; +use Filament\Auth\MultiFactor\Email\EmailAuthentication; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -13,7 +17,6 @@ use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; -use Filament\Support\Colors\Color; use Filament\Widgets\AccountWidget; use Filament\Widgets\FilamentInfoWidget; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -21,18 +24,61 @@ use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; +use Illuminate\Support\Facades\Log; use Illuminate\View\Middleware\ShareErrorsFromSession; +use Inerba\DbConfig\DbConfig; use Jacobtims\FilamentLogger\FilamentLoggerPlugin; class DashPanelProvider extends PanelProvider { + private string $panelId; + private array $panelConfig; + + // Panel configuration properties + private bool $panelDefault; + private string $panelPath; + private string $panelBrandName; + private string $panelFont; + private array $panelColor; + private bool $panelLogin; + private bool $panelRegistration; + private bool $panelProfile; + private bool $panelSPA; + private bool $panelSPAPrefetch; + private array $panelSPAExceptions; + private bool $panelBrandLogoShow; + private string $panelBrandLogoLight; + private string $panelBrandLogoDark; + private ?string $panelBrandLogoHeight; + private string $panelFavicon; + + // MFA configuration properties + private bool $panelMFA; + private string $panelMFAType; + private string $panelMFABrandName; + private bool $panelMFARecoverable; + private bool $panelMFARecoveryCodeRegenerate; + private int $panelMFACodeWindow; + private int $panelMFARecoveryCodeCount; + private int $panelMFAEmailCodeExpiry; + private bool $panelMFARequired; + + /** + * Configure the Filament panel. + */ public function panel(Panel $panel): Panel { - $panel = $panel - ->default() - ->id('dash') - ->path(config('filament-php.route')) - ->colors(config('filament-php.colors')) + $this->panelId = 'dash'; + $this->loadConfiguration(); + + $panel = $panel + ->default($this->panelDefault) + ->id($this->panelId) + ->path($this->panelPath) + ->colors($this->panelColor) + ->brandName($this->panelBrandName) + ->font($this->panelFont) + ->favicon(asset($this->panelFavicon)) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages') ->pages([ @@ -62,30 +108,106 @@ class DashPanelProvider extends PanelProvider FilamentLogViewerPlugin::make(), FilamentMailsPlugin::make()->canManageMails(function (): bool { $user = Auth::user(); - - // Allow access for users with specific roles - if ($user->hasRole('admin')) { - return true; - } - // Allow access for users with specific permissions - // Restrict access for all other users - return (bool) $user->hasPermissionTo('manage mails'); + return $user->hasRole('admin') || $user->hasPermissionTo('manage mails'); }), ]) - ->routes(fn () => FilamentMails::routes()); + ->routes(fn() => FilamentMails::routes()); - if (config('filament-php.enable_login')) { - $panel->login(); - } - - if (config('filament-php.enable_registration')) { - $panel->register(); - } - - if (config('filament-php.enable_profile')) { - $panel->profile(); - } + $this->applyConditionalConfiguration($panel); return $panel; } + + private function loadConfiguration(): void + { + try { + $this->panelConfig = DbConfig::getGroup('panel'); + } catch (\Exception $e) { + Log::error($e->getMessage()); + $this->panelConfig = []; + } + + // Load basic panel configuration + $this->panelDefault = $this->getConfig('panel_default') === $this->panelId; + $this->panelPath = $this->getConfig("panel_route_{$this->panelId}", $this->panelId); + $this->panelBrandName = $this->getConfig("panel_name_{$this->panelId}", config('app.name')); + $this->panelFont = $this->getConfig("panel_font_{$this->panelId}", 'Poppins'); + $this->panelColor = ColorHelper::getPanelColor($this->panelId); + + // Load feature flags + $this->panelLogin = $this->getConfig("panel_enable_login_{$this->panelId}", true); + $this->panelRegistration = $this->getConfig("panel_enable_registration_{$this->panelId}", false); + $this->panelProfile = $this->getConfig("panel_enable_profile_{$this->panelId}", false); + + // Load SPA configuration + $this->panelSPA = $this->getConfig("panel_spa_{$this->panelId}", false); + $this->panelSPAPrefetch = $this->getConfig("panel_spa_prefetch_{$this->panelId}", false); + $this->panelSPAExceptions = $this->getConfig("panel_spa_exceptions_{$this->panelId}", []); + + // Load branding configuration + $this->panelBrandLogoShow = $this->getConfig("panel_show_logo_{$this->panelId}") === 'show'; + $this->panelBrandLogoLight = $this->getConfig("panel_logo_light_{$this->panelId}", 'favicon.svg'); + $this->panelBrandLogoDark = $this->getConfig("panel_logo_dark_{$this->panelId}", 'favicon.svg'); + $this->panelBrandLogoHeight = $this->getConfig("panel_logo_height_{$this->panelId}"); + $this->panelFavicon = $this->getConfig("panel_favicon_{$this->panelId}", 'favicon.svg'); + + // Load MFA configuration + $this->panelMFA = $this->getConfig("panel_enable_mfa_{$this->panelId}", false); + $this->panelMFAType = $this->getConfig("panel_mfa_type_{$this->panelId}", 'app'); + $this->panelMFABrandName = $this->getConfig("panel_mfa_app_brand_name_{$this->panelId}", config('app.name')); + $this->panelMFARecoverable = $this->getConfig("panel_mfa_app_recoverable_{$this->panelId}", false); + $this->panelMFARecoveryCodeRegenerate = $this->getConfig("panel_mfa_app_recovery_code_regeneratable_{$this->panelId}", false); + $this->panelMFACodeWindow = $this->getConfig("panel_mfa_app_code_window_{$this->panelId}", 4); + $this->panelMFARecoveryCodeCount = $this->getConfig("panel_mfa_app_recovery_code_count_{$this->panelId}", 8); + $this->panelMFAEmailCodeExpiry = $this->getConfig("panel_mfa_email_code_expiry_{$this->panelId}", 2); + $this->panelMFARequired = $this->getConfig("panel_mfa_is_required_{$this->panelId}") === 'yes'; + } + + private function applyConditionalConfiguration(Panel $panel): void + { + $actions = [ + 'login' => [$this->panelLogin, fn() => $panel->login()], + 'register' => [$this->panelRegistration, fn() => $panel->registration()], + 'profile' => [$this->panelProfile, fn() => $panel->profile()], + 'spa' => [$this->panelSPA, fn() => $panel + ->spa(hasPrefetching: $this->panelSPAPrefetch) + ->spaUrlExceptions(exceptions: $this->panelSPAExceptions) + ], + 'logo' => [$this->panelBrandLogoShow, fn() => $panel + ->brandLogo(asset($this->panelBrandLogoLight)) + ->darkModeBrandLogo(asset($this->panelBrandLogoDark)) + ->brandLogoHeight($this->panelBrandLogoHeight) + ], + ]; + + foreach ($actions as [$condition, $callback]) { + $condition && $callback(); + } + + $this->configureMFA($panel); + } + + private function getConfig(string $key, $default = null) + { + return ArrayHelper::getValueFromArray($key, $this->panelConfig) ?? $default; + } + + private function configureMFA(Panel $panel): void + { + if (!$this->panelMFA) { + return; + } + + $authentication = $this->panelMFAType === 'app' + ? AppAuthentication::make() + ->brandName($this->panelMFABrandName) + ->recoverable($this->panelMFARecoverable) + ->recoveryCodeCount($this->panelMFARecoveryCodeCount) + ->regenerableRecoveryCodes($this->panelMFARecoveryCodeRegenerate) + ->codeWindow($this->panelMFACodeWindow) + : EmailAuthentication::make() + ->codeExpiryMinutes($this->panelMFAEmailCodeExpiry); + + $panel->multiFactorAuthentication([$authentication], isRequired: $this->panelMFARequired); + } } diff --git a/database/migrations/2025_10_01_000059_add_app_auth_fields_to_users_table.php b/database/migrations/2025_10_01_000059_add_app_auth_fields_to_users_table.php new file mode 100644 index 0000000..846993f --- /dev/null +++ b/database/migrations/2025_10_01_000059_add_app_auth_fields_to_users_table.php @@ -0,0 +1,34 @@ +text('app_authentication_secret')->nullable(); + $table->text('app_authentication_recovery_codes')->nullable(); + $table->boolean('has_email_authentication')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'app_authentication_secret', + 'app_authentication_recovery_codes', + 'has_email_authentication', + ]); + }); + } +}; diff --git a/resources/views/filament/pages/panel-settings.blade.php b/resources/views/filament/pages/panel-settings.blade.php new file mode 100644 index 0000000..f8c515a --- /dev/null +++ b/resources/views/filament/pages/panel-settings.blade.php @@ -0,0 +1,8 @@ + +
+ {{ $this->form }} + + Last update: {{ $this->lastUpdatedAt(timezone: 'UTC', format: 'F j, Y, H:i:s') . ' UTC' ?? 'Never' }} + +
+
\ No newline at end of file