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
stursby7mo 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
ZeroCharistmasOP7mo ago
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
awcodes7mo 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
stursby7mo ago
@ZeroCharistmas something like this? https://cln.sh/TDKGwNsW
CleanShot 2024-06-12 at 19.12.46
Video uploaded to CleanShot Cloud
stursby
stursby7mo ago
and then that secret field is stored encrytped in the DB
No description
stursby
stursby7mo 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
awcodes7mo 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
stursby7mo 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
awcodes7mo ago
No doubt. It’s a very specific use case which is why I brought up the idea of a custom field.
stursby
stursby7mo ago
yep, could probably just bundle all that up into one nice field!
awcodes
awcodes7mo ago
Your solution is clever though.
stursby
stursby7mo ago
ObfuscatedText::make() 😂
awcodes
awcodes7mo ago
ShowThisButOnlyShiwThisUnderASpecificCondition::make()
stursby
stursby7mo ago
lol, much more obvious now
awcodes
awcodes7mo ago
Forgot the ‘Input’ in the name. Sorry.
stursby
stursby7mo 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
awcodes7mo ago
A macro would be cool though. TextInput::make()->obfuscate() Seems like a viable plugin to me that has potential for core include.
stursby
stursby7mo ago
@ZeroCharistmas were you able to check out my demo? Does that work for ya?
ZeroCharistmas
ZeroCharistmasOP7mo ago
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
awcodes7mo 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
ZeroCharistmasOP7mo ago
I like that idea.
No description
ZeroCharistmas
ZeroCharistmasOP7mo ago
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
Shavik7mo ago
Glad you got it working!
ZeroCharistmas
ZeroCharistmasOP6mo ago
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)
);
});
Want results from more Discord servers?
Add your server