Initial commit
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
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
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// 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,
|
||||
}, ""
|
||||
}
|
||||
Reference in New Issue
Block a user