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:
@@ -353,7 +353,17 @@ func conformCases() []conformCase {
|
||||
return e.send(t, http.MethodGet, "/settings/conform/schema", nil, true)
|
||||
}, into[cabana.Envelope[cabana.FormView]](), nil},
|
||||
{"GET /settings/{code}", 200, "cabana.Envelope-cabana_SettingsResult", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
|
||||
return e.send(t, http.MethodGet, "/settings/conform", nil, true)
|
||||
rec := e.send(t, http.MethodGet, "/settings/conform", nil, true)
|
||||
// Phase 18 D-13: the password field is absent from data and
|
||||
// reported in meta.secrets.
|
||||
var body cabana.Envelope[cabana.SettingsResult]
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, leaked := body.Data.Data["api_key"]; leaked || body.Data.Meta == nil || body.Data.Meta.Secrets["api_key"] != cabana.SecretEmpty {
|
||||
t.Fatalf("settings secrets = %s", rec.Body.String())
|
||||
}
|
||||
return rec
|
||||
}, into[cabana.Envelope[cabana.SettingsResult]](), nil},
|
||||
{"PUT /settings/{code}", 200, "cabana.Envelope-cabana_SettingsResult", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
|
||||
return e.send(t, http.MethodPut, "/settings/conform", map[string]any{"enabled": true}, true)
|
||||
@@ -1060,12 +1070,13 @@ func (conformPart) AttachRelations() []attach.Relation {
|
||||
}
|
||||
|
||||
type conformSettings struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Enabled bool `gorm:"column:enabled"`
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Enabled bool `gorm:"column:enabled"`
|
||||
APIKey lagoon.Encrypted `gorm:"column:api_key;type:text"`
|
||||
}
|
||||
|
||||
func (conformSettings) TableName() string { return "cabana_conform_settings" }
|
||||
func (conformSettings) Fillable() []string { return []string{"enabled"} }
|
||||
func (conformSettings) Fillable() []string { return []string{"enabled", "api_key"} }
|
||||
func (conformSettings) Rules() map[string]string { return map[string]string{} }
|
||||
|
||||
type conformPlugin struct{ stamp string }
|
||||
@@ -1438,6 +1449,9 @@ parts:
|
||||
label: Enabled
|
||||
type: switch
|
||||
default: 0
|
||||
api_key:
|
||||
label: API key
|
||||
type: password
|
||||
`),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user