From 779ee72ab5bb0ed19ef03bbad9c875c6b787b4d2 Mon Sep 17 00:00:00 2001 From: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:40:25 +0000 Subject: [PATCH] Fix EditProfile --- app/Filament/Pages/Auth/EditProfile.php | 587 ++++++++++++------------ 1 file changed, 292 insertions(+), 295 deletions(-) diff --git a/app/Filament/Pages/Auth/EditProfile.php b/app/Filament/Pages/Auth/EditProfile.php index 10dba78ef..5b6af77fa 100644 --- a/app/Filament/Pages/Auth/EditProfile.php +++ b/app/Filament/Pages/Auth/EditProfile.php @@ -18,7 +18,6 @@ use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; use DateTimeZone; use Exception; -use Filament\Actions; use Filament\Actions\Action; use Filament\Forms\Components\FileUpload; use Filament\Schemas\Components\Grid; @@ -34,6 +33,8 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; use Filament\Schemas\Components\Utilities\Get; use Filament\Notifications\Notification; +use Filament\Schemas\Components\Actions; +use Filament\Schemas\Schema; use Filament\Support\Colors\Color; use Filament\Support\Enums\Width; use Filament\Support\Exceptions\Halt; @@ -64,170 +65,168 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile /** * @throws Exception */ - protected function getForms(): array + public function form(Schema $schema): Schema { $oauthProviders = collect(OAuthProvider::get())->filter(fn (OAuthProvider $provider) => $provider->isEnabled())->all(); - return [ - 'form' => $this->form( - $this->makeForm() + return $schema + ->components([ + Tabs::make()->persistTabInQueryString() ->schema([ - Tabs::make()->persistTabInQueryString() + Tab::make(trans('profile.tabs.account')) + ->icon('tabler-user') ->schema([ - Tab::make(trans('profile.tabs.account')) - ->icon('tabler-user') - ->schema([ - TextInput::make('username') - ->label(trans('profile.username')) - ->disabled() + TextInput::make('username') + ->label(trans('profile.username')) + ->disabled() + ->readOnly() + ->dehydrated(false) + ->maxLength(255) + ->unique(ignoreRecord: true) + ->autofocus(), + TextInput::make('email') + ->prefixIcon('tabler-mail') + ->label(trans('profile.email')) + ->email() + ->required() + ->maxLength(255) + ->unique(ignoreRecord: true), + TextInput::make('password') + ->label(trans('profile.password')) + ->password() + ->prefixIcon('tabler-password') + ->revealable(filament()->arePasswordsRevealable()) + ->rule(Password::default()) + ->autocomplete('new-password') + ->dehydrated(fn ($state): bool => filled($state)) + ->dehydrateStateUsing(fn ($state): string => Hash::make($state)) + ->live(debounce: 500) + ->same('passwordConfirmation'), + TextInput::make('passwordConfirmation') + ->label(trans('profile.password_confirmation')) + ->password() + ->prefixIcon('tabler-password-fingerprint') + ->revealable(filament()->arePasswordsRevealable()) + ->required() + ->visible(fn (Get $get): bool => filled($get('password'))) + ->dehydrated(false), + Select::make('timezone') + ->label(trans('profile.timezone')) + ->required() + ->prefixIcon('tabler-clock-pin') + ->default('UTC') + ->selectablePlaceholder(false) + ->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz])) + ->searchable() + ->native(false), + Select::make('language') + ->label(trans('profile.language')) + ->required() + ->prefixIcon('tabler-flag') + ->live() + ->default('en') + ->selectablePlaceholder(false) + ->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state]))) + ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()) + ->native(false), + FileUpload::make('avatar') + ->avatar() + ->acceptedFileTypes(['image/png']) + ->directory('avatars') + ->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png') + /* TODO ->hintAction(function (FileUpload $fileUpload) { + $path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png'; + + return Action::make('remove_avatar') + ->icon('tabler-photo-minus') + ->iconButton() + ->hidden(fn () => !$fileUpload->getDisk()->exists($path)) + ->action(fn () => $fileUpload->getDisk()->delete($path)) + ; + }) */, + ]), + + Tab::make(trans('profile.tabs.oauth')) + ->icon('tabler-brand-oauth') + ->visible(count($oauthProviders) > 0) + ->schema(function () use ($oauthProviders) { + $actions = []; + + foreach ($oauthProviders as $oauthProvider) { + + $id = $oauthProvider->getId(); + $name = $oauthProvider->getName(); + + $unlink = array_key_exists($id, $this->getUser()->oauth ?? []); + + $actions[] = Action::make("oauth_$id") + ->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name) + ->icon($unlink ? 'tabler-unlink' : 'tabler-link') + // TODO ->color(Color::hex($oauthProvider->getHexColor())) + ->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) { + if ($unlink) { + $oauth = auth()->user()->oauth; + unset($oauth[$id]); + + $updateService->handle(auth()->user(), ['oauth' => $oauth]); + + $this->fillForm(); + + Notification::make() + ->title(trans('profile.unlinked', ['name' => $name])) + ->success() + ->send(); + } else { + redirect(Socialite::with($id)->redirect()->getTargetUrl()); + } + }); + } + + return [Actions::make($actions)]; + }), + + Tab::make(trans('profile.tabs.2fa')) + ->icon('tabler-shield-lock') + ->schema(function (TwoFactorSetupService $setupService) { + if ($this->getUser()->use_totp) { + return [ + TextEntry::make('2fa-already-enabled') + ->label(trans('profile.2fa_enabled')), + Textarea::make('backup-tokens') + ->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens")) + ->rows(10) ->readOnly() ->dehydrated(false) - ->maxLength(255) - ->unique(ignoreRecord: true) - ->autofocus(), - TextInput::make('email') - ->prefixIcon('tabler-mail') - ->label(trans('profile.email')) - ->email() - ->required() - ->maxLength(255) - ->unique(ignoreRecord: true), - TextInput::make('password') - ->label(trans('profile.password')) - ->password() - ->prefixIcon('tabler-password') - ->revealable(filament()->arePasswordsRevealable()) - ->rule(Password::default()) - ->autocomplete('new-password') - ->dehydrated(fn ($state): bool => filled($state)) - ->dehydrateStateUsing(fn ($state): string => Hash::make($state)) - ->live(debounce: 500) - ->same('passwordConfirmation'), - TextInput::make('passwordConfirmation') - ->label(trans('profile.password_confirmation')) - ->password() - ->prefixIcon('tabler-password-fingerprint') - ->revealable(filament()->arePasswordsRevealable()) - ->required() - ->visible(fn (Get $get): bool => filled($get('password'))) - ->dehydrated(false), - Select::make('timezone') - ->label(trans('profile.timezone')) - ->required() - ->prefixIcon('tabler-clock-pin') - ->default('UTC') - ->selectablePlaceholder(false) - ->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz])) - ->searchable() - ->native(false), - Select::make('language') - ->label(trans('profile.language')) - ->required() - ->prefixIcon('tabler-flag') - ->live() - ->default('en') - ->selectablePlaceholder(false) - ->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state]))) - ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()) - ->native(false), - FileUpload::make('avatar') - ->visible(fn () => config('panel.filament.uploadable-avatars')) - ->avatar() - ->acceptedFileTypes(['image/png']) - ->directory('avatars') - ->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png') - ->hintAction(function (FileUpload $fileUpload) { - $path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png'; + ->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens")) + ->helperText(trans('profile.backup_help')) + ->label(trans('profile.backup_codes')), + TextInput::make('2fa-disable-code') + ->label(trans('profile.disable_2fa')) + ->helperText(trans('profile.disable_2fa_help')), + ]; + } - return Action::make('remove_avatar') - ->icon('tabler-photo-minus') - ->iconButton() - ->hidden(fn () => !$fileUpload->getDisk()->exists($path)) - ->action(fn () => $fileUpload->getDisk()->delete($path)); - }), - ]), + ['image_url_data' => $url, 'secret' => $secret] = cache()->remember( + "users.{$this->getUser()->id}.2fa.state", + now()->addMinutes(5), fn () => $setupService->handle($this->getUser()) + ); - Tab::make(trans('profile.tabs.oauth')) - ->icon('tabler-brand-oauth') - ->visible(count($oauthProviders) > 0) - ->schema(function () use ($oauthProviders) { - $actions = []; - - foreach ($oauthProviders as $oauthProvider) { - - $id = $oauthProvider->getId(); - $name = $oauthProvider->getName(); - - $unlink = array_key_exists($id, $this->getUser()->oauth ?? []); - - $actions[] = Action::make("oauth_$id") - ->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name) - ->icon($unlink ? 'tabler-unlink' : 'tabler-link') - ->color(Color::hex($oauthProvider->getHexColor())) - ->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) { - if ($unlink) { - $oauth = auth()->user()->oauth; - unset($oauth[$id]); - - $updateService->handle(auth()->user(), ['oauth' => $oauth]); - - $this->fillForm(); - - Notification::make() - ->title(trans('profile.unlinked', ['name' => $name])) - ->success() - ->send(); - } else { - redirect(Socialite::with($id)->redirect()->getTargetUrl()); - } - }); - } - - return [Actions::make($actions)]; - }), - - Tab::make(trans('profile.tabs.2fa')) - ->icon('tabler-shield-lock') - ->schema(function (TwoFactorSetupService $setupService) { - if ($this->getUser()->use_totp) { - return [ - TextEntry::make('2fa-already-enabled') - ->label(trans('profile.2fa_enabled')), - Textarea::make('backup-tokens') - ->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens")) - ->rows(10) - ->readOnly() - ->dehydrated(false) - ->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens")) - ->helperText(trans('profile.backup_help')) - ->label(trans('profile.backup_codes')), - TextInput::make('2fa-disable-code') - ->label(trans('profile.disable_2fa')) - ->helperText(trans('profile.disable_2fa_help')), - ]; - } - - ['image_url_data' => $url, 'secret' => $secret] = cache()->remember( - "users.{$this->getUser()->id}.2fa.state", - now()->addMinutes(5), fn () => $setupService->handle($this->getUser()) - ); - - $options = new QROptions([ - 'svgLogo' => public_path('pelican.svg'), - 'svgLogoScale' => 0.05, - 'addLogoSpace' => true, - 'logoSpaceWidth' => 13, - 'logoSpaceHeight' => 13, - 'version' => Version::AUTO, - // 'outputInterface' => QRSvgWithLogo::class, - 'outputBase64' => false, - 'eccLevel' => EccLevel::H, // ECC level H is necessary when using logos - 'addQuietzone' => true, - // 'drawLightModules' => true, - 'connectPaths' => true, - 'drawCircularModules' => true, - // 'circleRadius' => 0.45, - 'svgDefs' => ' + $options = new QROptions([ + 'svgLogo' => public_path('pelican.svg'), + 'svgLogoScale' => 0.05, + 'addLogoSpace' => true, + 'logoSpaceWidth' => 13, + 'logoSpaceHeight' => 13, + 'version' => Version::AUTO, + // 'outputInterface' => QRSvgWithLogo::class, + 'outputBase64' => false, + 'eccLevel' => EccLevel::H, // ECC level H is necessary when using logos + 'addQuietzone' => true, + // 'drawLightModules' => true, + 'connectPaths' => true, + 'drawCircularModules' => true, + // 'circleRadius' => 0.45, + 'svgDefs' => ' @@ -238,165 +237,163 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile .light{fill: #000;} ]]> ', - ]); + ]); - // https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php + // https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php - $image = (new QRCode($options))->render($url); + $image = (new QRCode($options))->render($url); - return [ - TextEntry::make('qr') - ->label(trans('profile.scan_qr')) - ->content(fn () => new HtmlString(" + return [ + TextEntry::make('qr') + ->label(trans('profile.scan_qr')) + ->state(fn () => new HtmlString("
$image
")) - ->helperText(trans('profile.setup_key') .': '. $secret), - TextInput::make('2facode') - ->label(trans('profile.code')) - ->requiredWith('2fapassword') - ->helperText(trans('profile.code_help')), - TextInput::make('2fapassword') - ->label(trans('profile.current_password')) - ->requiredWith('2facode') - ->currentPassword() - ->password(), - ]; - }), + ->helperText(trans('profile.setup_key') .': '. $secret), + TextInput::make('2facode') + ->label(trans('profile.code')) + ->requiredWith('2fapassword') + ->helperText(trans('profile.code_help')), + TextInput::make('2fapassword') + ->label(trans('profile.current_password')) + ->requiredWith('2facode') + ->currentPassword() + ->password(), + ]; + }), - Tab::make(trans('profile.tabs.api_keys')) - ->icon('tabler-key') - ->schema([ - Grid::make('name')->columns(5)->schema([ - Section::make(trans('profile.create_key'))->columnSpan(3)->schema([ - TextInput::make('description') - ->label(trans('profile.description')) - ->live(), - TagsInput::make('allowed_ips') - ->label(trans('profile.allowed_ips')) - ->live() - ->splitKeys([',', ' ', 'Tab']) - ->placeholder('127.0.0.1 or 192.168.1.1') - ->helperText(trans('profile.allowed_ips_help')) - ->columnSpanFull(), - ])->headerActions([ - Action::make('Create') - ->label(trans('filament-actions::create.single.modal.actions.create.label')) - ->disabled(fn (Get $get) => $get('description') === null) - ->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'], panel: 'app')) - ->action(function (Get $get, Action $action, User $user) { - $token = $user->createToken( - $get('description'), - $get('allowed_ips'), - ); + Tab::make(trans('profile.tabs.api_keys')) + ->icon('tabler-key') + ->schema([ + Grid::make(5)->schema([ + Section::make(trans('profile.create_key'))->columnSpan(3)->schema([ + TextInput::make('description') + ->label(trans('profile.description')) + ->live(), + TagsInput::make('allowed_ips') + ->label(trans('profile.allowed_ips')) + ->live() + ->splitKeys([',', ' ', 'Tab']) + ->placeholder('127.0.0.1 or 192.168.1.1') + ->helperText(trans('profile.allowed_ips_help')) + ->columnSpanFull(), + ])->headerActions([ + Action::make('Create') + ->label(trans('filament-actions::create.single.modal.actions.create.label')) + ->disabled(fn (Get $get) => $get('description') === null) + ->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'], panel: 'app')) + ->action(function (Get $get, Action $action, User $user) { + $token = $user->createToken( + $get('description'), + $get('allowed_ips'), + ); - Activity::event('user:api-key.create') - ->subject($token->accessToken) - ->property('identifier', $token->accessToken->identifier) - ->log(); + Activity::event('user:api-key.create') + ->subject($token->accessToken) + ->property('identifier', $token->accessToken->identifier) + ->log(); - Notification::make() - ->title(trans('profile.key_created')) - ->body($token->accessToken->identifier . $token->plainTextToken) - ->persistent() - ->success() - ->send(); + Notification::make() + ->title(trans('profile.key_created')) + ->body($token->accessToken->identifier . $token->plainTextToken) + ->persistent() + ->success() + ->send(); - $action->success(); - }), - ]), - Section::make(trans('profile.keys'))->label(trans('profile.keys'))->columnSpan(2)->schema([ - Repeater::make('keys') - ->label('') - ->relationship('apiKeys') - ->addable(false) - ->itemLabel(fn ($state) => $state['identifier']) - ->deleteAction(function (Action $action) { - $action->requiresConfirmation()->action(function (array $arguments, Repeater $component) { - $items = $component->getState(); - $key = $items[$arguments['item']]; - ApiKey::find($key['id'] ?? null)?->delete(); - - unset($items[$arguments['item']]); - - $component->state($items); - - $component->callAfterStateUpdated(); - }); - }) - ->schema(fn () => [ - TextEntry::make('adf')->label(fn (ApiKey $key) => $key->memo), - ]), - ]), - ]), + $action->success(); + }), ]), - - Tab::make(trans('profile.tabs.ssh_keys')) - ->icon('tabler-lock-code') - ->hidden(), - - Tab::make(trans('profile.tabs.activity')) - ->icon('tabler-history') - ->schema([ - Repeater::make('activity') + Section::make(trans('profile.keys'))->label(trans('profile.keys'))->columnSpan(2)->schema([ + Repeater::make('keys') ->label('') - ->deletable(false) + ->relationship('apiKeys') ->addable(false) - ->relationship(null, function (Builder $query) { - $query->orderBy('timestamp', 'desc'); + ->itemLabel(fn ($state) => $state['identifier']) + ->deleteAction(function (Action $action) { + $action->requiresConfirmation()->action(function (array $arguments, Repeater $component) { + $items = $component->getState(); + $key = $items[$arguments['item']]; + ApiKey::find($key['id'] ?? null)?->delete(); + + unset($items[$arguments['item']]); + + $component->state($items); + + $component->callAfterStateUpdated(); + }); }) ->schema([ - TextEntry::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())), + TextEntry::make('adf')->label(fn (ApiKey $key) => $key->memo), ]), ]), + ]), + ]), - Tab::make(trans('profile.tabs.customization')) - ->icon('tabler-adjustments') + Tab::make(trans('profile.tabs.ssh_keys')) + ->icon('tabler-lock-code') + ->hidden(), + + Tab::make(trans('profile.tabs.activity')) + ->icon('tabler-history') + ->schema([ + Repeater::make('activity') + ->label('') + ->deletable(false) + ->addable(false) + ->relationship(null, function (Builder $query) { + $query->orderBy('timestamp', 'desc'); + }) ->schema([ - Section::make(trans('profile.dashboard')) - ->collapsible() - ->icon('tabler-dashboard') - ->schema([ - ToggleButtons::make('dashboard_layout') - ->label(trans('profile.dashboard_layout')) - ->inline() - ->required() - ->options([ - 'grid' => trans('profile.grid'), - 'table' => trans('profile.table'), - ]), - ]), - Section::make(trans('profile.console')) - ->collapsible() - ->icon('tabler-brand-tabler') - ->schema([ - TextInput::make('console_rows') - ->label(trans('profile.rows')) - ->minValue(1) - ->numeric() - ->required() - ->columnSpan(1) - ->default(30), - // Select::make('console_font') - // ->label(trans('profile.font')) - // ->hidden() //TODO - // ->columnSpan(1), - TextInput::make('console_font_size') - ->label(trans('profile.font_size')) - ->columnSpan(1) - ->minValue(1) - ->numeric() - ->required() - ->default(14), - ]), + TextEntry::make('activity!')->label('')->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())), ]), ]), - ]) - ->operation('edit') - ->model($this->getUser()) - ->statePath('data') - ->inlineLabel(!static::isSimple()), - ), - ]; + + Tab::make(trans('profile.tabs.customization')) + ->icon('tabler-adjustments') + ->schema([ + Section::make(trans('profile.dashboard')) + ->collapsible() + ->icon('tabler-dashboard') + ->schema([ + ToggleButtons::make('dashboard_layout') + ->label(trans('profile.dashboard_layout')) + ->inline() + ->required() + ->options([ + 'grid' => trans('profile.grid'), + 'table' => trans('profile.table'), + ]), + ]), + Section::make(trans('profile.console')) + ->collapsible() + ->icon('tabler-brand-tabler') + ->schema([ + TextInput::make('console_rows') + ->label(trans('profile.rows')) + ->minValue(1) + ->numeric() + ->required() + ->columnSpan(1) + ->default(30), + // Select::make('console_font') + // ->label(trans('profile.font')) + // ->hidden() //TODO + // ->columnSpan(1), + TextInput::make('console_font_size') + ->label(trans('profile.font_size')) + ->columnSpan(1) + ->minValue(1) + ->numeric() + ->required() + ->default(14), + ]), + ]), + ]), + ]) + ->operation('edit') + ->model($this->getUser()) + ->statePath('data') + ->inlineLabel(!static::isSimple()); } protected function handleRecordUpdate(Model $record, array $data): Model