feat: add PanelSetting to the dash panel
This commit is contained in:
288
app/Filament/Pages/PanelSettings.php
Normal file
288
app/Filament/Pages/PanelSettings.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/Helper/ArrayHelper.php
Normal file
14
app/Helper/ArrayHelper.php
Normal 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
156
app/Helper/ColorHelper.php
Normal 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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,20 +2,23 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthenticationRecovery;
|
||||||
|
use Filament\Auth\MultiFactor\Email\Contracts\HasEmailAuthentication;
|
||||||
use Filament\Models\Contracts\FilamentUser;
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Illuminate\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
use Spatie\Permission\Traits\HasRoles;
|
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<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable, TwoFactorAuthenticatable, MustVerifyEmail, HasRoles;
|
use HasFactory, Notifiable, TwoFactorAuthenticatable, HasRoles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@@ -37,6 +40,8 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'password',
|
||||||
'remember_token',
|
'remember_token',
|
||||||
|
'app_authentication_secret',
|
||||||
|
'app_authentication_recovery_codes',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +54,9 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'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');
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Providers\Filament;
|
namespace App\Providers\Filament;
|
||||||
|
|
||||||
|
use App\Helper\ArrayHelper;
|
||||||
|
use App\Helper\ColorHelper;
|
||||||
use Auth;
|
use Auth;
|
||||||
use Backstage\FilamentMails\Facades\FilamentMails;
|
use Backstage\FilamentMails\Facades\FilamentMails;
|
||||||
use Backstage\FilamentMails\FilamentMailsPlugin;
|
use Backstage\FilamentMails\FilamentMailsPlugin;
|
||||||
use Boquizo\FilamentLogViewer\FilamentLogViewerPlugin;
|
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\Authenticate;
|
||||||
use Filament\Http\Middleware\AuthenticateSession;
|
use Filament\Http\Middleware\AuthenticateSession;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
@@ -13,7 +17,6 @@ use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
|||||||
use Filament\Pages\Dashboard;
|
use Filament\Pages\Dashboard;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Filament\PanelProvider;
|
use Filament\PanelProvider;
|
||||||
use Filament\Support\Colors\Color;
|
|
||||||
use Filament\Widgets\AccountWidget;
|
use Filament\Widgets\AccountWidget;
|
||||||
use Filament\Widgets\FilamentInfoWidget;
|
use Filament\Widgets\FilamentInfoWidget;
|
||||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||||
@@ -21,18 +24,61 @@ use Illuminate\Cookie\Middleware\EncryptCookies;
|
|||||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
use Inerba\DbConfig\DbConfig;
|
||||||
use Jacobtims\FilamentLogger\FilamentLoggerPlugin;
|
use Jacobtims\FilamentLogger\FilamentLoggerPlugin;
|
||||||
|
|
||||||
class DashPanelProvider extends PanelProvider
|
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
|
public function panel(Panel $panel): Panel
|
||||||
{
|
{
|
||||||
|
$this->panelId = 'dash';
|
||||||
|
$this->loadConfiguration();
|
||||||
|
|
||||||
$panel = $panel
|
$panel = $panel
|
||||||
->default()
|
->default($this->panelDefault)
|
||||||
->id('dash')
|
->id($this->panelId)
|
||||||
->path(config('filament-php.route'))
|
->path($this->panelPath)
|
||||||
->colors(config('filament-php.colors'))
|
->colors($this->panelColor)
|
||||||
|
->brandName($this->panelBrandName)
|
||||||
|
->font($this->panelFont)
|
||||||
|
->favicon(asset($this->panelFavicon))
|
||||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
||||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
|
||||||
->pages([
|
->pages([
|
||||||
@@ -62,30 +108,106 @@ class DashPanelProvider extends PanelProvider
|
|||||||
FilamentLogViewerPlugin::make(),
|
FilamentLogViewerPlugin::make(),
|
||||||
FilamentMailsPlugin::make()->canManageMails(function (): bool {
|
FilamentMailsPlugin::make()->canManageMails(function (): bool {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
return $user->hasRole('admin') || $user->hasPermissionTo('manage mails');
|
||||||
// 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');
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->routes(fn () => FilamentMails::routes());
|
->routes(fn() => FilamentMails::routes());
|
||||||
|
|
||||||
if (config('filament-php.enable_login')) {
|
$this->applyConditionalConfiguration($panel);
|
||||||
$panel->login();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config('filament-php.enable_registration')) {
|
|
||||||
$panel->register();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config('filament-php.enable_profile')) {
|
|
||||||
$panel->profile();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
8
resources/views/filament/pages/panel-settings.blade.php
Normal file
8
resources/views/filament/pages/panel-settings.blade.php
Normal 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>
|
||||||
Reference in New Issue
Block a user