$this->form( $this->makeForm() ->schema([ Tabs::make()->persistTabInQueryString() ->schema([ Tab::make('Account') ->label(trans('strings.account')) ->icon('tabler-user') ->schema([ TextInput::make('username') ->label(trans('strings.username')) ->disabled() ->readOnly() ->dehydrated(false) ->maxLength(255) ->unique(ignoreRecord: true) ->autofocus(), TextInput::make('email') ->prefixIcon('tabler-mail') ->label(trans('strings.email')) ->email() ->required() ->maxLength(255) ->unique(ignoreRecord: true), TextInput::make('password') ->label(trans('strings.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('strings.password_confirmation')) ->password() ->prefixIcon('tabler-password-fingerprint') ->revealable(filament()->arePasswordsRevealable()) ->required() ->visible(fn (Get $get): bool => filled($get('password'))) ->dehydrated(false), Select::make('timezone') ->required() ->prefixIcon('tabler-clock-pin') ->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz])) ->searchable(), Select::make('language') ->label(trans('strings.language')) ->required() ->prefixIcon('tabler-flag') ->live() ->default('en') ->helperText(fn (User $user, $state) => new HtmlString($user->isLanguageTranslated($state) ? '' : " Your language ($state) has not been translated yet! But never fear, you can help fix that by contributing directly here. ") ) ->options(fn (User $user) => $user->getAvailableLanguages()), ]), Tab::make('2FA') ->icon('tabler-shield-lock') ->schema(function () { if ($this->getUser()->use_totp) { return [ Placeholder::make('2fa-already-enabled') ->label('Two Factor Authentication is currently 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('These will not be shown again!') ->label('Backup Tokens:'), TextInput::make('2fa-disable-code') ->label('Disable 2FA') ->helperText('Enter your current 2FA code to disable Two Factor Authentication'), ]; } /** @var TwoFactorSetupService */ $setupService = app(TwoFactorSetupService::class); ['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, ]); // https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php // QROptions // @phpstan-ignore property.protected $options->version = Version::AUTO; // $options->outputInterface = QRSvgWithLogo::class; // @phpstan-ignore property.protected $options->outputBase64 = false; // @phpstan-ignore property.protected $options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos // @phpstan-ignore property.protected $options->addQuietzone = true; // $options->drawLightModules = true; // @phpstan-ignore property.protected $options->connectPaths = true; // @phpstan-ignore property.protected $options->drawCircularModules = true; // $options->circleRadius = 0.45; // @phpstan-ignore property.protected $options->svgDefs = ' '; $image = (new QRCode($options))->render($url); return [ Placeholder::make('qr') ->label('Scan QR Code') ->content(fn () => new HtmlString("
$image
")) ->helperText('Setup Key: '. $secret), TextInput::make('2facode') ->label('Code') ->requiredWith('2fapassword') ->helperText('Scan the QR code above using your two-step authentication app, then enter the code generated.'), TextInput::make('2fapassword') ->label('Current Password') ->requiredWith('2facode') ->currentPassword() ->password() ->helperText('Enter your current password to verify.'), ]; }), Tab::make('API Keys') ->icon('tabler-key') ->schema([ Grid::make('asdf')->columns(5)->schema([ Section::make('Create API Key')->columnSpan(3)->schema([ TextInput::make('description') ->live(), TagsInput::make('allowed_ips') ->live() ->splitKeys([',', ' ', 'Tab']) ->placeholder('Example: 127.0.0.1 or 192.168.1.1') ->label('Whitelisted IP\'s') ->helperText('Press enter to add a new IP address or leave blank to allow any IP address') ->columnSpanFull(), ])->headerActions([ Action::make('Create') ->disabled(fn (Get $get) => $get('description') === null) ->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab'])) ->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(); $action->success(); }), ]), Section::make('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 () => [ Placeholder::make('adf')->label(fn (ApiKey $key) => $key->memo), ]), ]), ]), ]), Tab::make('SSH Keys') ->icon('tabler-lock-code') ->schema([ Placeholder::make('Coming soon!'), ]), Tab::make('Activity') ->icon('tabler-history') ->schema([ Repeater::make('activity') ->deletable(false) ->addable(false) ->relationship(null, function (Builder $query) { $query->orderBy('timestamp', 'desc'); }) ->schema([ Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())), ]), ]), ]), ]) ->operation('edit') ->model($this->getUser()) ->statePath('data') ->inlineLabel(!static::isSimple()), ), ]; } protected function handleRecordUpdate(Model $record, array $data): Model { if (! $record instanceof User) { return $record; } if ($token = $data['2facode'] ?? null) { /** @var ToggleTwoFactorService $service */ $service = resolve(ToggleTwoFactorService::class); $tokens = $service->handle($record, $token, true); cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15)); $this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']); } if ($token = $data['2fa-disable-code'] ?? null) { /** @var ToggleTwoFactorService $service */ $service = resolve(ToggleTwoFactorService::class); $service->handle($record, $token, false); cache()->forget("users.$record->id.2fa.state"); } return parent::handleRecordUpdate($record, $data); } public function exception(Exception $e, Closure $stopPropagation): void { if ($e instanceof TwoFactorAuthenticationTokenInvalid) { Notification::make() ->title('Invalid 2FA Code') ->body($e->getMessage()) ->color('danger') ->icon('tabler-2fa') ->danger() ->send(); $stopPropagation(); } } }