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,170 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/interpres/v2"
|
||||
"sourcedock.dev/petrbalvin/volumen/internal/payloads"
|
||||
"sourcedock.dev/petrbalvin/volumen/internal/templates"
|
||||
)
|
||||
|
||||
// templateFields are the editor inputs a template may pre-fill beyond
|
||||
// its title, slug, tags and body; anything else in the fields box is a
|
||||
// typo waiting to seed every new post with a key nobody reads.
|
||||
var templateFields = map[string]bool{
|
||||
"lang": true, "author": true, "fediverse_creator": true,
|
||||
"doi": true, "orcid": true,
|
||||
"series": true, "series_order": true,
|
||||
"excerpt": true, "cover": true, "cover_alt": true, "cover_caption": true,
|
||||
}
|
||||
|
||||
// parseTemplateFields reads the fields box: key = value lines in TOML,
|
||||
// each key an editor field. unknown names the first key outside the
|
||||
// allowed set; err reports text that is not a small TOML document.
|
||||
func parseTemplateFields(text string) (fields map[string]string, unknown string, err error) {
|
||||
if strings.TrimSpace(text) == "" {
|
||||
return nil, "", nil
|
||||
}
|
||||
data, err := interpres.ParseMap([]byte(text))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("template fields must be key = value lines")
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, key := range slices.Sorted(maps.Keys(data)) {
|
||||
if !templateFields[key] {
|
||||
return nil, key, nil
|
||||
}
|
||||
value := data[key]
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
if text, isString := value.(string); isString {
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
out[key] = text
|
||||
continue
|
||||
}
|
||||
if number, isInt := value.(int64); isInt {
|
||||
out[key] = strconv.FormatInt(number, 10)
|
||||
continue
|
||||
}
|
||||
out[key] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, "", nil
|
||||
}
|
||||
return out, "", nil
|
||||
}
|
||||
|
||||
func (a *Admin) handleSettingsTemplateCreate(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireCSRF(w, r) {
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(r.PostFormValue("name"))
|
||||
if name == "" {
|
||||
a.renderSettings(w, r, a.tr(r, "Template name is required."), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
fields, unknown, err := parseTemplateFields(r.PostFormValue("fields"))
|
||||
switch {
|
||||
case err != nil:
|
||||
a.renderSettings(w, r, a.tr(r, "Template fields must be key = value TOML lines."), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
case unknown != "":
|
||||
a.renderSettings(w, r, a.trf(r, "Unknown template field %s.", unknown), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
if _, err := a.deps.Templates.Add(templates.PostTemplate{
|
||||
Name: name,
|
||||
Tags: payloads.ParseTags(r.PostFormValue("tags")),
|
||||
Body: r.PostFormValue("body"),
|
||||
Title: strings.TrimSpace(r.PostFormValue("title")),
|
||||
Slug: strings.TrimSpace(r.PostFormValue("slug")),
|
||||
Fields: fields,
|
||||
}); err != nil {
|
||||
a.renderSettings(w, r, a.trf(r, "That template could not be added: %s", err.Error()), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
a.renderSettings(w, r, "", a.tr(r, "Template added."), http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *Admin) handleSettingsTemplateDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireCSRF(w, r) {
|
||||
return
|
||||
}
|
||||
if err := a.deps.Templates.Delete(r.PathValue("name")); err != nil {
|
||||
a.renderSettings(w, r, a.trf(r, "The template could not be deleted: %s", err.Error()), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
a.renderSettings(w, r, "", a.tr(r, "Template deleted."), http.StatusOK)
|
||||
}
|
||||
|
||||
// --- backup export / import -------------------------------------------------
|
||||
|
||||
// handleSettingsWebhookTest delivers a ping inline and reports the
|
||||
// outcome from the delivery it produced, not from the shared history a
|
||||
// concurrent delivery could reshuffle.
|
||||
func (a *Admin) handleSettingsWebhookTest(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireCSRF(w, r) {
|
||||
return
|
||||
}
|
||||
if a.deps.Webhooks == nil {
|
||||
a.renderSettings(w, r, a.tr(r, "No webhooks configured."), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
// The list is taken once: a settings change that reshuffles it between
|
||||
// the bounds check and the fetch would test a different hook than the
|
||||
// one the form named.
|
||||
hooks := a.deps.Webhooks.Hooks()
|
||||
index, err := strconv.Atoi(r.PathValue("index"))
|
||||
if err != nil || index < 0 || index >= len(hooks) {
|
||||
a.renderSettings(w, r, a.tr(r, "Webhook not found."), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
hook := hooks[index]
|
||||
delivery := a.deps.Webhooks.TestHook(hook)
|
||||
a.record(r, "webhook.tested", hook.URL, nil)
|
||||
notice := a.tr(r, "Test delivery failed.")
|
||||
if delivery.Status == "ok" {
|
||||
notice = a.tr(r, "Test delivery sent.")
|
||||
}
|
||||
a.renderSettings(w, r, "", notice, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *Admin) handleSettingsTokenCreate(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireCSRF(w, r) {
|
||||
return
|
||||
}
|
||||
created, raw, err := a.deps.Tokens.Create(r.PostFormValue("name"), r.PostForm["scope"])
|
||||
if err != nil {
|
||||
a.renderSettings(w, r, a.trf(r, "The token could not be created: %s", err.Error()), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
a.record(r, "token.created", created.Name, nil)
|
||||
data := a.settingsData(r)
|
||||
data.NewToken = raw
|
||||
a.renderPage(w, r, "settings.html", data, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *Admin) handleSettingsTokenDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireCSRF(w, r) {
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
notice := a.tr(r, "Token revoked.")
|
||||
if !a.deps.Tokens.Revoke(name) {
|
||||
notice = a.tr(r, "That token was not found.")
|
||||
} else {
|
||||
a.record(r, "token.revoked", name, nil)
|
||||
}
|
||||
a.renderSettings(w, r, "", notice, http.StatusOK)
|
||||
}
|
||||
Reference in New Issue
Block a user