Text input with partially obfuscated data.

I have a few fields that I need to partially obfuscate in the edit page of my resource. When the input hasn't been changed, I'd like it to not replace the original value with the obfuscated version after save. I've tried using formatStateUsing to replace the value with null or with the obfuscated version, but that changes the original value on save. I can set the placeholder to the obfuscated value, but I still need a way to have the field empty when it's not been changed so the placeholder shows the obfuscated value, still without overwriting when not specifically changed. Also I will love you forever if you can give me a way to clear the input when the user starts typing in the field.
Solution:
You could check the state of the field with the mutate before update lifecycle hook. Then if that field contains asterisks then remove it from the state. Then it won’t get updated in the db.
Jump to solution
25 Replies
stursby
stursby4w ago
@ZeroCharistmas so if I'm understanding it correctly, you want a field a user can enter into, and then after save, on the edit page, that field value is obfuscated? But if they try to edit it, it clears out, and shows plain text again?
ZeroCharistmas
The only time I ever want the value to be fully visible to a user is at the time it is entered fresh either in a create or edit action(when replacing the value). It should be obfuscated on the server side such that the original value doesn't show up in the html. After it is set, I want it to be partially obfuscatedbsuch that a user can infer that it is accurate. I don't want the method of obfuscation to change the saved value to its obfuscated version when other parts of the record are changed.
awcodes
awcodes4w ago
You’re probably going to need a custom input field then. This gets into a lot of specific/edge case functionality that would be incredibly difficult to handle from a framework standpoint.
stursby
stursby4w ago
@ZeroCharistmas something like this? https://cln.sh/TDKGwNsW
CleanShot 2024-06-12 at 19.12.46
Video uploaded to CleanShot Cloud
stursby
stursby4w ago
and then that secret field is stored encrytped in the DB
No description
stursby
stursby4w ago
@ZeroCharistmas here's my little demo repo: https://github.com/stursby/filament-obfuscated-input (most of the relevant code is in this commit -> https://github.com/stursby/filament-obfuscated-input/commit/914f66b9de30dfa5f0182064d6120b9c367b72f1) @awcodes feel free to chime in if you can think of any better ways to handle something like this!
GitHub
GitHub - stursby/filament-obfuscated-input
Contribute to stursby/filament-obfuscated-input development by creating an account on GitHub.
awcodes
awcodes4w ago
If it works it works. I still think it could be done with a mask though. 😅 It certainly looks ok to me though the way you implemented it, but seems like a lot to have to implement it on the edit / create record class instead of at the field level.
stursby
stursby4w ago
yea, I think since he said he wanted to see the input text while creating / editing and replacing with a new value, and then in all other cases it's partially obfuscated... wans't sure how to handle all that logic in a mask TBH 😂
awcodes
awcodes4w ago
No doubt. It’s a very specific use case which is why I brought up the idea of a custom field.
stursby
stursby4w ago
yep, could probably just bundle all that up into one nice field!
awcodes
awcodes4w ago
Your solution is clever though.
stursby
stursby4w ago
ObfuscatedText::make() 😂
awcodes
awcodes4w ago
ShowThisButOnlyShiwThisUnderASpecificCondition::make()
stursby
stursby4w ago
lol, much more obvious now
awcodes
awcodes4w ago
Forgot the ‘Input’ in the name. Sorry.
stursby
stursby4w ago
could chain on something like
ObfuscatedText::make('secret')
->obfuscatedOn(['edit'])
->revealCharacterCount(3)
->revealPosition('end')
ObfuscatedText::make('secret')
->obfuscatedOn(['edit'])
->revealCharacterCount(3)
->revealPosition('end')
similar to hiddenOn() 🤷‍♂️
awcodes
awcodes4w ago
A macro would be cool though. TextInput::make()->obfuscate() Seems like a viable plugin to me that has potential for core include.
stursby
stursby4w ago
@ZeroCharistmas were you able to check out my demo? Does that work for ya?
ZeroCharistmas
Today's been kinda unexpectedly crazy for me. I'll check tomorrow. Thanks! So your implementation is cool but it hits the same problem that mine is hitting. If the record is saved and the obfuscated input is left obfuscated, the secret field is overwritten with asterisks in the db(before encryption) I'ma try my hand at custom-field-smithy
Solution
awcodes
awcodes4w ago
You could check the state of the field with the mutate before update lifecycle hook. Then if that field contains asterisks then remove it from the state. Then it won’t get updated in the db.
ZeroCharistmas
I like that idea.
No description
ZeroCharistmas
Got a version of it working thanks to you guys and @Chris Reed! Because this is a field of a one-to-one relationship, the data didn't seem to be showing up in the edit page's mutateFormDataBeforeSave callback. Putting a before function on the input seems to be similar enough though, so I used that to disable dehydration on the component if it's null.
Forms\Components\TextInput::make('secret')
->label('secret')
// Clear the input field so the obfuscated value is shown in the placeholder.
->formatStateUsing(fn () => null)
// Shows an obfuscated version of the value in a way that doesn't affect the data during save.
->placeholder(fn ($record): string =>
preg_replace("/[^-](?=\\S{5})/", '*', $record->secret))
// Prevents updating the field if it is null.
->before(function ($component, $state) {
if(empty($state)) {$component->dehydrated(false);}
})
// Allows clearing the data because the input is ignored when empty.
->suffixAction(
Action::make('clearData')
->label('Clear Secret')
->icon('heroicon-m-x-mark')
->requiresConfirmation()
->modalDescription("Are you sure you would like to clear this field?")
->action(function ($record, $set) {
$set('secret', null);
$record->update(['secret' => null]);
})
)
Forms\Components\TextInput::make('secret')
->label('secret')
// Clear the input field so the obfuscated value is shown in the placeholder.
->formatStateUsing(fn () => null)
// Shows an obfuscated version of the value in a way that doesn't affect the data during save.
->placeholder(fn ($record): string =>
preg_replace("/[^-](?=\\S{5})/", '*', $record->secret))
// Prevents updating the field if it is null.
->before(function ($component, $state) {
if(empty($state)) {$component->dehydrated(false);}
})
// Allows clearing the data because the input is ignored when empty.
->suffixAction(
Action::make('clearData')
->label('Clear Secret')
->icon('heroicon-m-x-mark')
->requiresConfirmation()
->modalDescription("Are you sure you would like to clear this field?")
->action(function ($record, $set) {
$set('secret', null);
$record->update(['secret' => null]);
})
)
Shavik
Shavik3w ago
Glad you got it working!
ZeroCharistmas
Here it is as a macro:
TextInput::macro("obfuscate", function(string $replacement = "*", array $ignore = [], int $revealFirst = 0, int $revealLast = 0){
$ignoreString = empty(array_filter($ignore)) ? '.' : '[^'.implode($ignore).']';
$revealFirst += 1;
$regexPattern = "/{$ignoreString}(?=.{{$revealLast}})(?<=.{{$revealFirst}})/";
$oldPlaceholder = $this->getPlaceholder();
return $this->formatStateUsing(fn () => null)
// Shows an obfuscated version of the value in a way that doesn't affect the data after save.
->placeholder(function($record) use ($oldPlaceholder, $replacement, $regexPattern): string {
if(is_null($record) || is_null($record[$this->getName()])){
return is_null($oldPlaceholder) ? $this->getLabel() : $oldPlaceholder;
} else {
return preg_replace($regexPattern, $replacement, $record[$this->getName()]);
}
})
// Prevents updating the field if it is null.
->before(function ($component, $state) {
if(empty($state)) {$component->dehydrated(false);}
})
// Allows clearing the data because the input is ignored when empty.
->suffixAction(
Action::make('clearData')
->label("Clear {$this->getLabel()}")
->icon('heroicon-m-x-mark')
->tooltip("Clear {$this->getLabel()}")
->requiresConfirmation()
->modalDescription("Are you sure you would like to clear this field?")
->action(function ($record, $set) {
$set($this->getName(), null);
if(!is_null($record))
{
$record->update([$this->getName() => null]);
}
})
->disabled(fn($record) => $record === null)
->hidden(fn($record) => $record === null)
);
});
TextInput::macro("obfuscate", function(string $replacement = "*", array $ignore = [], int $revealFirst = 0, int $revealLast = 0){
$ignoreString = empty(array_filter($ignore)) ? '.' : '[^'.implode($ignore).']';
$revealFirst += 1;
$regexPattern = "/{$ignoreString}(?=.{{$revealLast}})(?<=.{{$revealFirst}})/";
$oldPlaceholder = $this->getPlaceholder();
return $this->formatStateUsing(fn () => null)
// Shows an obfuscated version of the value in a way that doesn't affect the data after save.
->placeholder(function($record) use ($oldPlaceholder, $replacement, $regexPattern): string {
if(is_null($record) || is_null($record[$this->getName()])){
return is_null($oldPlaceholder) ? $this->getLabel() : $oldPlaceholder;
} else {
return preg_replace($regexPattern, $replacement, $record[$this->getName()]);
}
})
// Prevents updating the field if it is null.
->before(function ($component, $state) {
if(empty($state)) {$component->dehydrated(false);}
})
// Allows clearing the data because the input is ignored when empty.
->suffixAction(
Action::make('clearData')
->label("Clear {$this->getLabel()}")
->icon('heroicon-m-x-mark')
->tooltip("Clear {$this->getLabel()}")
->requiresConfirmation()
->modalDescription("Are you sure you would like to clear this field?")
->action(function ($record, $set) {
$set($this->getName(), null);
if(!is_null($record))
{
$record->update([$this->getName() => null]);
}
})
->disabled(fn($record) => $record === null)
->hidden(fn($record) => $record === null)
);
});