feat: add PanelSetting to the dash panel

This commit is contained in:
idevakk
2025-10-02 19:48:45 +05:30
parent 47c6359663
commit 631143f160
7 changed files with 697 additions and 29 deletions

View File

@@ -0,0 +1,288 @@
<?php
namespace App\Filament\Pages;
use App\Helper\ColorHelper;
use BackedEnum;
use Filament\Facades\Filament;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Inerba\DbConfig\AbstractPageSettings;
use Filament\Schemas\Components;
use Filament\Schemas\Schema;
use Phiki\Phast\Text;
class PanelSettings extends AbstractPageSettings
{
/**
* @var array<string, mixed> | 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');
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Helper;
class ArrayHelper
{
/**
* Get value from array by key, return null if not found.
*/
public static function getValueFromArray(string $key, array $array)
{
return $array[$key] ?? null;
}
}

156
app/Helper/ColorHelper.php Normal file
View File

@@ -0,0 +1,156 @@
<?php
namespace App\Helper;
use Illuminate\Support\Facades\Log;
use JetBrains\PhpStorm\Pure;
class ColorHelper
{
/**
* Convert RGB to OKLab color space.
*/
public static function rgbToOklab($r, $g, $b)
{
// Normalize RGB values to the range [0, 1]
$r = $r / 255;
$g = $g / 255;
$b = $b / 255;
// Linearize RGB values
$r = $r <= 0.04045 ? $r / 12.92 : pow(($r + 0.055) / 1.055, 2.4);
$g = $g <= 0.04045 ? $g / 12.92 : pow(($g + 0.055) / 1.055, 2.4);
$b = $b <= 0.04045 ? $b / 12.92 : pow(($b + 0.055) / 1.055, 2.4);
// Convert to linear light values
$l = 0.4122214708 * $r + 0.5363325363 * $g + 0.0514459929 * $b;
$m = 0.2119034982 * $r + 0.6806995451 * $g + 0.1073969566 * $b;
$s = 0.0883024619 * $r + 0.2817188376 * $g + 0.6299787005 * $b;
// Apply the OKLab transformation
$l_ = pow($l, 1 / 3);
$m_ = pow($m, 1 / 3);
$s_ = pow($s, 1 / 3);
$L = 0.2104542553 * $l_ + 0.7936177850 * $m_ - 0.0040720468 * $s_;
$a = 1.9779984951 * $l_ - 2.4285922050 * $m_ + 0.4505937099 * $s_;
$b = 0.0259040371 * $l_ + 0.7827717662 * $m_ - 0.8086757660 * $s_;
return ['L' => $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 [];
}
}
}

View File

@@ -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();
}
}

View File

@@ -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
{
$this->panelId = 'dash';
$this->loadConfiguration();
$panel = $panel
->default()
->id('dash')
->path(config('filament-php.route'))
->colors(config('filament-php.colors'))
->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);
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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',
]);
});
}
};

View File

@@ -0,0 +1,8 @@
<x-filament-panels::page>
<form wire:submit="save" class="fi-page-content">
{{ $this->form }}
<small class="text-success">
Last update: {{ $this->lastUpdatedAt(timezone: 'UTC', format: 'F j, Y, H:i:s') . ' UTC' ?? 'Never' }}
</small>
</form>
</x-filament-panels::page>