feat(18-01): write-only settings secrets, richeditor and tabs on settings forms (task 3)
- type: password on a settings form binds a lagoon.Encrypted column as a write-only secret (any other column fails boot naming the field): the GET omits it from data and reports stored, env or empty per field in meta.secrets; on PUT an absent key or "" keeps the value, null clears it, a non-string is a 422; the answer never carries a value - cabana.SecretFallbacks (field to config key) gives the env state; cabana.SettingsMeta, SecretStored/SecretEnv/SecretEmpty exported - settings forms accept richeditor: the Put sanitizes through seaglass like a controller save and returns meta.notices; repeater and dependsOn stay refused (settingsFormOnly next to controllerFormOnly) - backend::lang secret.* phrases in en and pl - admin SPA: PasswordField secret status line (stored with Remove stored value, env, empty, pending clear with Undo), SettingsFormView tabs with error-tab activation and the secrets/notices meta, settings payload that never sends an untouched secret; regenerated OpenAPI and TS types; rebuilt dist - vault fixture, TestSettingsSecretCompile, Docker TestSettingsSecretSmoke and TestSettingsRichEditorSmoke, conform settings fixture with a password field; settings.md sections, forms.md notes, cabana README
This commit is contained in:
@@ -95,6 +95,44 @@ The settings row is the one with ID 1. Before it exists, the page shows the fiel
|
||||
|
||||
The admin API serves the page at `<prefix>/api/v1/settings/{code}` (values) and `.../settings/{code}/schema` (the form), and `GET .../settings` lists the pages the administrator may open.
|
||||
|
||||
## Write-only secrets
|
||||
|
||||
A `type: password` field on a settings form is a write-only secret: an API key, a webhook signing secret. Its model column must be a `lagoon.Encrypted` (or a pointer to one), so the value is encrypted at rest under `app.key`; any other column type stops the start-up naming the field.
|
||||
|
||||
```yaml
|
||||
fields:
|
||||
secret_key:
|
||||
label: acme.pay::lang.settings.secret_key
|
||||
type: password
|
||||
tab: acme.pay::lang.settings.tab_live
|
||||
```
|
||||
|
||||
- `GET /settings/{code}` never includes the field in `data`. It reports the field's state in `meta.secrets` instead: `stored` when the column holds a value, `env` when the column is empty and a fallback applies (below), or `empty`. The `meta` key is omitted when the form has no password field.
|
||||
- `PUT /settings/{code}` keeps the stored value when the key is absent or an empty string, clears it when the value is JSON `null`, and stores any other string through `lagoon.NewEncrypted`. A value that is neither a string nor `null` is a 422 on the field. The answer carries the new states in `meta.secrets` and never the values; cabana never logs a secret and never echoes it in an error.
|
||||
- A `required` password field is satisfied by a stored value, so an absent key does not fail the required check.
|
||||
|
||||
The admin shows the field empty on every load with a status line under it: a stored value with a "Remove stored value" button (the clear is deferred until Save and can be undone), the configuration fallback, or "Not set". Typing a value replaces the stored one on Save; an untouched field is not sent.
|
||||
|
||||
A settings model may implement `cabana.SecretFallbacks` to name, per field, the configuration key the application falls back to when the column is empty, so the page can say that a value from the server configuration is in use:
|
||||
|
||||
```go src=modules/cabana/settings_secret_test.go#secretSettings.SecretFallbacks
|
||||
// SecretFallbacks maps each secret field to the config key the application
|
||||
// reads when the column is empty; SUMMER_* variables map onto those keys.
|
||||
func (secretSettings) SecretFallbacks() map[string]string {
|
||||
return map[string]string{"api_key": "acme.secret.api_key"}
|
||||
}
|
||||
```
|
||||
|
||||
cabana only checks whether the key resolves to a non-empty value (`env` state); reading and using the fallback value stays the plugin's job. The states are the constants `cabana.SecretStored`, `cabana.SecretEnv` and `cabana.SecretEmpty`, and the meta shape is `cabana.SettingsMeta`.
|
||||
|
||||
## Rich text on a settings page
|
||||
|
||||
`type: richeditor` is accepted on a settings form. The settings save runs every submitted value through the same seaglass sanitizer as a controller save (see [Rich text](forms.md#rich-text)): clean HTML is stored byte for byte, otherwise the clean value is stored and the answer lists what was removed in `meta.notices`, which the admin shows under the field. A value that is not a string or `null` is a 422 on the field. `repeater` and `dependsOn` are still refused on settings forms, as they are on relation forms.
|
||||
|
||||
## Tabs
|
||||
|
||||
Settings fields take `tab` like record form fields. When any field declares one, the admin renders the tab strip in the page header, with the fields that have no `tab` on the default tab first; the form card sits inside the active tab's panel. After a 422 the first tab with an error is activated and shows its error count. Without any `tab` the page is unchanged.
|
||||
|
||||
## Reading settings in plugin code
|
||||
|
||||
Settings are an ordinary table, so plugin code reads them with GORM:
|
||||
|
||||
Reference in New Issue
Block a user