check / check (push) Successful in 3m14s
Pure code movement. Every declaration of internal/handlers/source_management.go moves unchanged into one of: webhook_list.go, webhook_create.go, webhook_detail.go, webhook_edit.go and webhook_delete.go for the webhook pages; event_log.go for the event log; entrypoint.go for the entrypoint handlers; target_create.go and target.go for the target handlers; and shared.go for the helpers several of them use. Only each file's package line and imports are new. Model: opus-5-5
246 lines
6.0 KiB
Go
246 lines
6.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
)
|
|
|
|
// HandleSourceEdit shows the form to edit a webhook.
|
|
func (h *Handlers) HandleSourceEdit() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := h.getUserID(r)
|
|
if !ok {
|
|
http.Redirect(
|
|
w, r, "/pages/login", http.StatusSeeOther,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
sourceID := chi.URLParam(r, "sourceID")
|
|
|
|
var webhook database.Webhook
|
|
|
|
err := h.db.DB().Where(
|
|
"id = ? AND user_id = ?", sourceID, userID,
|
|
).First(&webhook).Error
|
|
if err != nil {
|
|
h.renderError(w, r, http.StatusNotFound)
|
|
|
|
return
|
|
}
|
|
|
|
h.renderWebhookEdit(
|
|
w, r, &webhook,
|
|
webhook.Name, webhook.Description,
|
|
strconv.Itoa(webhook.RetentionDays),
|
|
"", http.StatusOK,
|
|
)
|
|
}
|
|
}
|
|
|
|
// HandleSourceEditSubmit handles the webhook edit form
|
|
// submission.
|
|
func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := h.getUserID(r)
|
|
if !ok {
|
|
http.Redirect(
|
|
w, r, "/pages/login", http.StatusSeeOther,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
sourceID := chi.URLParam(r, "sourceID")
|
|
|
|
h.renameMu.Lock()
|
|
defer h.renameMu.Unlock()
|
|
|
|
var webhook database.Webhook
|
|
|
|
err := h.db.DB().Where(
|
|
"id = ? AND user_id = ?", sourceID, userID,
|
|
).First(&webhook).Error
|
|
if err != nil {
|
|
h.renderError(w, r, http.StatusNotFound)
|
|
|
|
return
|
|
}
|
|
|
|
// The body size cap is enforced by the MaxBodySize
|
|
// middleware, which runs before CSRF parses the form.
|
|
err = r.ParseForm()
|
|
if err != nil {
|
|
h.renderError(w, r, http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
h.applyWebhookEdit(w, r, &webhook)
|
|
}
|
|
}
|
|
|
|
// applyWebhookEdit validates and saves webhook edits. A refused save
|
|
// shows the edit form again with the values submitted and the reason.
|
|
func (h *Handlers) applyWebhookEdit(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
webhook *database.Webhook,
|
|
) {
|
|
// The body size cap is enforced by the MaxBodySize middleware,
|
|
// which runs before CSRF parses the form.
|
|
name := r.PostFormValue("name")
|
|
description := r.PostFormValue("description")
|
|
retention := r.PostFormValue("retention_days")
|
|
|
|
if name == "" {
|
|
h.renderWebhookEdit(
|
|
w, r, webhook, name, description, retention,
|
|
"Name is required", http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
// An empty field falls back to the stored value, so submitting the
|
|
// form without touching retention leaves the policy alone.
|
|
retentionDays, errMsg := parseRetentionDays(
|
|
retention, webhook.RetentionDays,
|
|
)
|
|
if errMsg != "" {
|
|
h.renderWebhookEdit(
|
|
w, r, webhook, name, description, retention,
|
|
errMsg, http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
// edited is the webhook as the submission leaves it; webhook stays
|
|
// as stored, for the page shown again when the save is refused.
|
|
edited := *webhook
|
|
edited.Name = name
|
|
edited.Description = description
|
|
edited.RetentionDays = retentionDays
|
|
|
|
// A new name renames the archive files before it is saved (see
|
|
// delivery.Engine.Rename). If either step fails, the same targets'
|
|
// archives go back to the name that is still stored, without
|
|
// reading the main database again.
|
|
targets, err := h.renameWebhookArchives(
|
|
webhook.ID, webhook.Name, edited.Name,
|
|
)
|
|
if err == nil {
|
|
err = h.db.DB().Save(&edited).Error
|
|
}
|
|
|
|
if err != nil {
|
|
restoreErr := h.renameArchives(targets, webhook.Name)
|
|
if restoreErr != nil {
|
|
h.log.Error(
|
|
"failed to rename archives back",
|
|
"webhook_id", webhook.ID,
|
|
"error", restoreErr,
|
|
)
|
|
}
|
|
|
|
if errors.Is(err, delivery.ErrArchiveNameTaken) {
|
|
h.renderWebhookEdit(
|
|
w, r, webhook, name, description, retention,
|
|
"Not saved: "+err.Error()+
|
|
". Move that archive out of the data directory, "+
|
|
"its .db together with any -wal and -shm beside "+
|
|
"it, then save again.",
|
|
http.StatusConflict,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
h.serverError(w, r, "failed to update webhook", err)
|
|
|
|
return
|
|
}
|
|
|
|
http.Redirect(
|
|
w, r, withNotice("/hook/"+webhook.ID, webhookSaved),
|
|
http.StatusSeeOther,
|
|
)
|
|
}
|
|
|
|
// renderWebhookEdit renders the webhook edit page for the webhook as
|
|
// stored, its form showing name, description and retentionDays, with
|
|
// an optional error message above it.
|
|
func (h *Handlers) renderWebhookEdit(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
webhook *database.Webhook,
|
|
name, description, retentionDays, errMsg string,
|
|
status int,
|
|
) {
|
|
data := map[string]any{
|
|
tmplKeyWebhook: webhook,
|
|
tmplKeyError: errMsg,
|
|
"Name": name,
|
|
"Description": description,
|
|
"RetentionDays": retentionDays,
|
|
}
|
|
|
|
h.renderTemplateStatus(w, r, "source_edit.html", data, status)
|
|
}
|
|
|
|
// renameWebhookArchives renames the archive file of every database
|
|
// target of a webhook from the webhook name oldName to newName,
|
|
// keeping each target's own name. It does nothing when the name is
|
|
// unchanged. It returns the targets it read, so that a failed edit can
|
|
// move those same archives back with renameArchives.
|
|
func (h *Handlers) renameWebhookArchives(
|
|
webhookID, oldName, newName string,
|
|
) ([]database.Target, error) {
|
|
if h.archives == nil || oldName == newName {
|
|
return nil, nil
|
|
}
|
|
|
|
var targets []database.Target
|
|
|
|
err := h.db.DB().
|
|
Where(
|
|
"webhook_id = ? AND type = ?",
|
|
webhookID, database.TargetTypeDatabase,
|
|
).
|
|
Find(&targets).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return targets, h.renameArchives(targets, newName)
|
|
}
|
|
|
|
// renameArchives renames the archive file of each of the given
|
|
// database targets to the webhook name webhookName, keeping each
|
|
// target's own name. It tries every target even after one fails, so
|
|
// that moving the archives back after a failed edit leaves none under
|
|
// the new name, and returns every failure joined.
|
|
func (h *Handlers) renameArchives(
|
|
targets []database.Target, webhookName string,
|
|
) error {
|
|
var errs []error
|
|
|
|
for i := range targets {
|
|
err := h.archives.Rename(
|
|
targets[i].ID, webhookName, targets[i].Name,
|
|
)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|