Add a target edit form and reachable header/timeout fields (closes #127)
All checks were successful
check / check (push) Successful in 3m48s

A target's configuration was write-once: `internal/server/routes.go`
registered create, toggle and delete for targets but no edit route, so
correcting a typo in a destination meant deleting the target and
recreating it. Masking the stored value made that unrecoverable from
the UI.

Headers and timeout were worse than write-once. `HTTPTargetConfig` has
carried `Headers` and `Timeout` and the delivery path has honoured both,
but `buildURLTargetConfig` only ever wrote `{"url":...}` and no form
offered either field, so a destination needing an `Authorization` header
could not be configured through the UI at all.

Both paths now build their configuration through `buildTargetConfig`, so
an edited destination is SSRF-validated exactly as a new one is. The
destination check lives in one helper that both reach, leaving the guard's
entry point untouched.

The edit form pre-fills the stored destination and header values in full.
That is the one intentional exception to the masking rule, narrowed by
the route it lives on: `RequireAuth`, `NoCache`, and the webhook's
ownership check. `delivery.TargetView` is unchanged, so every other page
still masks.

A target's type stays fixed at creation: each type stores a different
configuration shape and its delivery history is recorded against the row,
so changing it is really a different target.

Header and timeout input that could not be delivered as written is
rejected rather than stored: a malformed line, an invalid name, a control
character in a value, a repeated name, a header the delivery engine
overwrites regardless, or a timeout that is not a whole number of seconds
within the ceiling. Storing input that provably never reaches the wire
would report a configuration that did not take effect.
This commit is contained in:
2026-08-20 04:24:27 +00:00
parent a13e5b7ded
commit 84878cd218
11 changed files with 1764 additions and 44 deletions

View File

@@ -69,18 +69,29 @@ func (s *Handlers) RenderTemplateForTest(
s.renderTemplate(w, r, pageTemplate, data)
}
// BuildSlackTargetConfigForTest exposes buildURLTargetConfig
// with the Slack target parameters for use in the
// handlers_test package.
// BuildSlackTargetConfigForTest exposes
// buildSlackTargetConfig for use in the handlers_test package.
func (s *Handlers) BuildSlackTargetConfigForTest(
w http.ResponseWriter,
r *http.Request,
targetURL string,
) (string, error) {
return s.buildURLTargetConfig(
w, r, targetURL, "webhookUrl",
"Webhook URL is required for Slack targets",
)
return s.buildSlackTargetConfig(w, r, targetURL)
}
// BuildHTTPTargetConfigForTest exposes buildHTTPTargetConfig
// for use in the handlers_test package, taking the form fields
// an HTTP target's configuration is built from.
func (s *Handlers) BuildHTTPTargetConfigForTest(
w http.ResponseWriter,
r *http.Request,
targetURL, headers, timeout string,
) (string, error) {
return s.buildHTTPTargetConfig(w, r, targetFormInput{
URL: targetURL,
Headers: headers,
Timeout: timeout,
})
}
// BuildDatabaseTargetConfigForTest exposes