check / check (push) Waiting to run
Adding or editing a database target now offers the archive expiry choices of the new webhook page (never, 1h, 12h, 24h, 30d, 90d, 365d) in place of a text field. The list is defined once, in internal/handlers/archive_expiry.go, and all three forms render it. The edit form starts on the expiry it shows: the stored one, or the submitted one after a refused save. One that is not among the choices is listed first as its own entry, so saving unchanged keeps it. The target list shows the expiry in plain units, such as "30 days" or "12 hours", or "never". Model: opus-5-5
120 lines
3.6 KiB
Go
120 lines
3.6 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// errUnknownTargetTypeForEdit is returned when a stored target has a
|
|
// type the edit form has no field set for.
|
|
var errUnknownTargetTypeForEdit = errors.New(
|
|
"unknown target type",
|
|
)
|
|
|
|
// TargetConfigForm is the UNMASKED projection of a target's stored
|
|
// configuration, for pre-filling the target edit form.
|
|
//
|
|
// It is the deliberate exception to the rule TargetView enforces
|
|
// everywhere else: TargetView exists so that no template can render
|
|
// a target's stored blob, because a destination URL's path segments
|
|
// and a header value are both routinely the credential. An operator
|
|
// cannot correct a value they cannot see, so the edit form — and
|
|
// only the edit form — is shown the full value.
|
|
//
|
|
// Everything that keeps that exception narrow lives at the call
|
|
// site: the route is behind RequireAuth and the webhook's ownership
|
|
// check, and its group sets NoCache so the rendered secret is not
|
|
// written to a shared cache. Do not reach for this type from any
|
|
// other page.
|
|
type TargetConfigForm struct {
|
|
// URL is the destination for an HTTP target and the webhook
|
|
// URL for a Slack target.
|
|
URL string
|
|
// Headers is the HTTP target's configured headers in the
|
|
// textarea representation, one "Name: value" per line.
|
|
Headers string
|
|
// Timeout is the HTTP target's per-request timeout in seconds,
|
|
// empty when unset.
|
|
Timeout string
|
|
// Expiry is the database (archive) target's row expiry.
|
|
Expiry string
|
|
}
|
|
|
|
// NewTargetConfigForm parses a target's stored configuration into
|
|
// the edit form's fields.
|
|
//
|
|
// A configuration that does not parse is an error rather than a
|
|
// zero-valued form that silently looks like a target with no
|
|
// settings. The caller shows the operator that the stored value
|
|
// could not be read, so that saving the form is understood as
|
|
// replacing it rather than preserving it.
|
|
func NewTargetConfigForm(
|
|
t *database.Target,
|
|
) (TargetConfigForm, error) {
|
|
switch t.Type {
|
|
case database.TargetTypeHTTP:
|
|
cfg, err := parseHTTPConfig(t.Config)
|
|
if err != nil {
|
|
return TargetConfigForm{}, err
|
|
}
|
|
|
|
return TargetConfigForm{
|
|
URL: cfg.URL,
|
|
Headers: FormatTargetHeaders(cfg.Headers),
|
|
Timeout: FormatTargetTimeout(cfg.Timeout),
|
|
}, nil
|
|
case database.TargetTypeSlack:
|
|
cfg, err := parseSlackConfig(t.Config)
|
|
if err != nil {
|
|
return TargetConfigForm{}, err
|
|
}
|
|
|
|
return TargetConfigForm{URL: cfg.WebhookURL}, nil
|
|
case database.TargetTypeDatabase:
|
|
return databaseConfigForm(t.Config)
|
|
case database.TargetTypeLog:
|
|
// The log target takes no configuration.
|
|
return TargetConfigForm{}, nil
|
|
default:
|
|
return TargetConfigForm{}, fmt.Errorf(
|
|
"%w: %q", errUnknownTargetTypeForEdit, t.Type,
|
|
)
|
|
}
|
|
}
|
|
|
|
// databaseConfigForm parses an archive target's optional expiry.
|
|
// An absent, empty or never expiry yields an empty expiry, on which
|
|
// the edit form starts at never; saving it unchanged stores never,
|
|
// which means the same as an empty expiry. An expiry that is set
|
|
// but not a valid duration is an error, not a blank field.
|
|
func databaseConfigForm(
|
|
configJSON string,
|
|
) (TargetConfigForm, error) {
|
|
if configJSON == "" {
|
|
return TargetConfigForm{}, nil
|
|
}
|
|
|
|
var cfg databaseTargetConfig
|
|
|
|
err := json.Unmarshal([]byte(configJSON), &cfg)
|
|
if err != nil {
|
|
return TargetConfigForm{}, fmt.Errorf(
|
|
"parsing config JSON: %w", err,
|
|
)
|
|
}
|
|
|
|
if cfg.Expiry == "" || cfg.Expiry == archiveExpiryNever {
|
|
return TargetConfigForm{}, nil
|
|
}
|
|
|
|
err = ValidateArchiveExpiry(cfg.Expiry)
|
|
if err != nil {
|
|
return TargetConfigForm{}, err
|
|
}
|
|
|
|
return TargetConfigForm{Expiry: cfg.Expiry}, nil
|
|
}
|