norman-huth/nova-secret-field
A Laravel Nova field for securely storing and managing secrets like API keys and passwords. It masks sensitive values in the UI, encrypts data at rest, and lets you update credentials without exposing them. Ideal for admin panels that need safe secret handling without revealing values to users.
This is a single-purpose Nova field: a text input rendered as dots with an eye toggle to reveal the current value. Treat it as a UI component, not a security feature. With 3 stars, zero dependents, and an archived repo, you are effectively adopting an unmaintained snippet — plan to vendor the code rather than depend on Packagist.
Minimal steps:
src/ and resources/js/components/. You need three facts:
Muetze42, package nova-secret-field; the class is under Muetze42\NovaSecretField),$component string it registers (must match the Nova.booting component name in dist/js/field.js),composer.json constraint on laravel/nova — last release was 2022-07-12, so it targets Nova 4 and will not have been touched for Nova 5.composer require muetze42/nova-secret-field and immediately fork it into app/Nova/Fields/Secret.php, or add it as a VCS repo pinned to a commit. Do not let a fresh composer update decide your fate.use Muetze42\NovaSecretField\Secret; // verify against src/
public function fields(Request $request)
{
return [
Secret::make('API Token')
->rules('required', 'string', 'max:255')
->hideFromIndex()
->hideFromDetail()
->help('Paste the token; use the eye icon to verify before saving.'),
];
}
Start with the field hidden from index and detail. The reveal toggle only makes sense on the form, where the user is the one who typed the value.
Store encrypted, not plaintext. Pair the field with a model cast so the masked input maps to encrypted storage:
protected $casts = ['api_token' => 'encrypted'];
The field is a plain input; it does no hashing and no encryption for you.
Do not clobber existing secrets on a blank submit. This is the single most important pattern. Without it, an admin editing an unrelated attribute wipes the token. Either handle it in fillUsing:
Secret::make('API Token')
->fillUsing(function ($request, $model, $attribute) {
if (filled($request->input($attribute))) {
$model->{$attribute} = $request->input($attribute);
}
})
…or make the field ->nullable() and short-circuit in a model mutator (setApiTokenAttribute ignores empty strings). Nova's built-in Password field has this "only fill when non-empty" behaviour baked in; check whether this package's fill() does the same before assuming.
Mask the detail and index views instead of hiding them entirely. If reviewers need to know a secret exists, show a fixed mask via displayUsing on a separate read-only field, and keep the actual secret field onlyOnForms():
Text::make('API Token', 'api_token')
->displayUsing(fn ($value) => $value ? '••••••••' : '—')
->onlyOnDetail(),
Secret::make('API Token', 'api_token')
->onlyOnForms()
->rules('nullable', 'string'),
Using a distinct Text field for display avoids depending on the custom Vue component in the read-only path, so if the compiled asset breaks you still have a working detail page.
Gate the reveal behind policy. ->canSee(fn ($request) => $request->user()->can('manageSecrets')) on the form field keeps the masked input out of the DOM for users who should never touch it. Note this hides the input, not the payload — see Gotchas.
Pair with an action for rotation. A "Rotate token" action that generates a value server-side and returns it once is a better workflow than repeatedly revealing a long-lived secret in a form:
// in an Action's handle()
$model->update(['api_token' => Str::random(64)]);
return Action::message('Token rotated. Copy it from the field now.');
Testing. The reveal toggle is pure front-end, so test the write path at the HTTP level (actingAs($admin)->put('/nova-api/settings/1', [...])) and assert the token is present/absent after a partial update. That test is where you'll catch the blank-submit bug.
type between password and text. Anything hidden only by the mask is fully readable in the network tab. Never place it on index or detail unless you actually intend to transmit the secret to that screen.dist/js/field.js may fail to register or render unstyled. Verify by loading any form using the field and checking the console for a missing-component warning.npm install && npm run prod (or dev) and confirm the service provider's Nova::script(...)/Nova::style(...) paths point at the rebuilt dist/.sortable() and searchable() are meaningless here. Encrypted or masked values can't be sorted or searched server-side; adding those flags produces misleading UI.border-gray-300 or bg-white often looks broken in dark mode. Check the Vue component's classes before shipping, or accept the cosmetic debt.null. $request->input() returns '' for an untouched blank input, not null. Use filled() rather than ! is_null() when guarding writes.component(), resolve(), fill(). Rather than patching around it, copy it to app/Nova/Fields/Secret.php, rename the $component to your own namespace, and own it. That removes the abandoned dependency entirely and lets you add the non-empty fill() guard directly.dist/js/field.js doesn't match $component. Confirm registration with Nova.booting((app, store) => { app.component('...', require('./components/Field.vue').default) }) in resources/js/field.js.Text::make('API Token')->withMeta(['type' => 'password']) gets you a masked input with zero dependencies; a 20-line custom field gets you the toggle. For a 3-star archived package with no maintainer, the build-versus-buy math is not close.How can I help you explore Laravel packages today?