Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
140 lines
4.8 KiB
Go
140 lines
4.8 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
|
|
package admin
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
|
|
"sourcedock.dev/petrbalvin/volumen/internal/payloads"
|
|
"sourcedock.dev/petrbalvin/volumen/internal/webhooks"
|
|
)
|
|
|
|
// fileHooks reads the admin-managed hook store.
|
|
func (a *Admin) fileHooks() ([]webhooks.Webhook, error) {
|
|
if a.deps.WebhooksFile == "" {
|
|
return nil, nil
|
|
}
|
|
return webhooks.LoadFile(a.deps.WebhooksFile)
|
|
}
|
|
|
|
// refreshWebhooks re-saves the store and applies the merged hook set to
|
|
// the manager, so a settings change delivers without a restart.
|
|
func (a *Admin) refreshWebhooks(hooks []webhooks.Webhook) error {
|
|
if err := webhooks.SaveFile(a.deps.WebhooksFile, hooks); err != nil {
|
|
return err
|
|
}
|
|
merged := make([]webhooks.Webhook, 0, len(a.deps.StaticWebhooks)+len(hooks))
|
|
merged = append(merged, a.deps.StaticWebhooks...)
|
|
merged = append(merged, hooks...)
|
|
a.deps.Webhooks.SetHooks(merged)
|
|
return nil
|
|
}
|
|
|
|
// handleSettingsWebhookAdd adds one endpoint to the store. Config-declared
|
|
// hooks are the operator's business and stay read-only; this list is the
|
|
// admin's to manage.
|
|
func (a *Admin) handleSettingsWebhookAdd(w http.ResponseWriter, r *http.Request) {
|
|
if !a.requireCSRF(w, r) {
|
|
return
|
|
}
|
|
file, err := a.fileHooks()
|
|
if err != nil {
|
|
a.renderSettings(w, r, a.trf(r, "The webhook store could not be read: %s", err.Error()), "", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
hook, problem := validateHookInput(
|
|
r.PostFormValue("url"), strings.TrimSpace(r.PostFormValue("secret")),
|
|
r.PostFormValue("events"), r.PostFormValue("enabled") == "on",
|
|
append(slices.Clone(a.deps.StaticWebhooks), file...),
|
|
)
|
|
if problem != "" {
|
|
a.renderSettings(w, r, a.tr(r, problem), "", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
if err := a.refreshWebhooks(append(file, hook)); err != nil {
|
|
a.renderSettings(w, r, a.trf(r, "The webhook could not be saved: %s", err.Error()), "", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
a.record(r, "webhook.added", hook.URL, nil)
|
|
a.renderSettings(w, r, "", a.tr(r, "Webhook added."), http.StatusOK)
|
|
}
|
|
|
|
// handleSettingsWebhookToggle flips one stored hook's enabled flag. The
|
|
// URL names the hook, because the row the form was rendered from may no
|
|
// longer be at its old index by the time the POST lands.
|
|
func (a *Admin) handleSettingsWebhookToggle(w http.ResponseWriter, r *http.Request) {
|
|
a.mutateStoredHook(w, r, "webhook.updated", "Webhook updated.",
|
|
func(hooks []webhooks.Webhook, url string) ([]webhooks.Webhook, bool) {
|
|
for i, hook := range hooks {
|
|
if hook.URL == url {
|
|
hooks[i].Enabled = !hook.Enabled
|
|
return hooks, true
|
|
}
|
|
}
|
|
return hooks, false
|
|
})
|
|
}
|
|
|
|
func (a *Admin) handleSettingsWebhookDelete(w http.ResponseWriter, r *http.Request) {
|
|
a.mutateStoredHook(w, r, "webhook.deleted", "Webhook removed.",
|
|
func(hooks []webhooks.Webhook, url string) ([]webhooks.Webhook, bool) {
|
|
for i, hook := range hooks {
|
|
if hook.URL == url {
|
|
return slices.Delete(hooks, i, i+1), true
|
|
}
|
|
}
|
|
return hooks, false
|
|
})
|
|
}
|
|
|
|
// mutateStoredHook applies a change to the stored hook the form's url
|
|
// field names, re-saves, and refreshes the manager.
|
|
func (a *Admin) mutateStoredHook(w http.ResponseWriter, r *http.Request, auditAction, notice string, apply func([]webhooks.Webhook, string) ([]webhooks.Webhook, bool)) {
|
|
if !a.requireCSRF(w, r) {
|
|
return
|
|
}
|
|
file, err := a.fileHooks()
|
|
if err != nil {
|
|
a.renderSettings(w, r, a.trf(r, "The webhook store could not be read: %s", err.Error()), "", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
target := r.PostFormValue("url")
|
|
changed, ok := apply(file, target)
|
|
if !ok {
|
|
a.renderSettings(w, r, a.tr(r, "Webhook not found."), "", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
if err := a.refreshWebhooks(changed); err != nil {
|
|
a.renderSettings(w, r, a.trf(r, "The webhook could not be saved: %s", err.Error()), "", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
a.record(r, auditAction, target, nil)
|
|
a.renderSettings(w, r, "", a.tr(r, notice), http.StatusOK)
|
|
}
|
|
|
|
// validateHookInput checks the add form: an absolute http(s) URL no
|
|
// configured hook already uses, an optional secret, and the event
|
|
// filter as a comma-separated list (empty delivers everything).
|
|
func validateHookInput(raw, secret, events string, enabled bool, existing []webhooks.Webhook) (webhooks.Webhook, string) {
|
|
raw = strings.TrimSpace(raw)
|
|
u, err := url.Parse(raw)
|
|
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
|
return webhooks.Webhook{}, "That URL is not a valid http(s) endpoint."
|
|
}
|
|
for _, hook := range existing {
|
|
if hook.URL == raw {
|
|
return webhooks.Webhook{}, "That URL is already configured."
|
|
}
|
|
}
|
|
return webhooks.Webhook{
|
|
URL: raw,
|
|
Secret: secret,
|
|
Events: payloads.ParseTags(events),
|
|
Enabled: enabled,
|
|
}, ""
|
|
}
|