check / check (push) Waiting to run
`internal/handlers/source_management.go` had grown to about 2,370 lines holding the webhook, event log, entrypoint and target handlers, so unrelated units had to wait on each other to touch it. It is split, as pure code movement, into files named for what they hold: `webhook_list.go`, `webhook_create.go`, `webhook_detail.go`, `webhook_edit.go`, `webhook_delete.go`, `event_log.go`, `entrypoint.go`, `target_create.go`, `target_delete.go`, `target_toggle.go` and `shared.go`. No function body, signature, comment or behaviour changed. The README's file tree and log-line caveat, and one middleware comment, name the new files. Model: opus-5-5
134 lines
3.3 KiB
Go
134 lines
3.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/reqtls"
|
|
)
|
|
|
|
// HandleSourceDetail shows details for a specific webhook.
|
|
func (h *Handlers) HandleSourceDetail() 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.renderSourceDetail(w, r, webhook, targetFormInput{}, "")
|
|
}
|
|
}
|
|
|
|
// renderSourceDetail loads and renders a source detail page. With a
|
|
// targetErr, it is the page shown again for a refused add target
|
|
// form: it answers 400, and the form opens on targetForm's type with
|
|
// its values and the message.
|
|
func (h *Handlers) renderSourceDetail(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
webhook database.Webhook,
|
|
targetForm targetFormInput,
|
|
targetErr string,
|
|
) {
|
|
var entrypoints []database.Entrypoint
|
|
|
|
h.db.DB().Where(
|
|
"webhook_id = ?", webhook.ID,
|
|
).Find(&entrypoints)
|
|
|
|
var targets []database.Target
|
|
|
|
h.db.DB().Where(
|
|
"webhook_id = ?", webhook.ID,
|
|
).Find(&targets)
|
|
|
|
entrypointViews := NewEntrypointViews(entrypoints)
|
|
|
|
var events []RecentEventView
|
|
|
|
if h.dbMgr.DBExists(webhook.ID) {
|
|
webhookDB, err := h.dbMgr.GetDB(webhook.ID)
|
|
if err != nil {
|
|
h.serverError(w, r, "failed to get webhook database", err)
|
|
|
|
return
|
|
}
|
|
|
|
events, err = loadRecentEvents(
|
|
webhookDB, webhook.ID, singleHTTPTargetID(targets),
|
|
)
|
|
if err != nil {
|
|
h.serverError(w, r, "failed to load recent events", err)
|
|
|
|
return
|
|
}
|
|
|
|
err = addEntrypointEvents(
|
|
webhookDB, &webhook, entrypointViews, time.Now(),
|
|
)
|
|
if err != nil {
|
|
h.serverError(w, r, "failed to count entrypoint events", err)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
scheme := "http"
|
|
if reqtls.IsTLS(r) {
|
|
scheme = "https"
|
|
}
|
|
|
|
// The host is the client's Host header, unvalidated. It is
|
|
// inert only because source_detail.html renders BaseURL as
|
|
// text, inside a <code> element and in an entrypoint's delete
|
|
// prompt; putting it in an href or any other URL context
|
|
// needs it constrained first.
|
|
baseURL := scheme + "://" + r.Host
|
|
|
|
// The template calls Webhook methods, which take pointer
|
|
// receivers; html/template cannot address a value stored in a map.
|
|
data := map[string]any{
|
|
tmplKeyWebhook: &webhook,
|
|
// Targets are projected to a display-safe view: a
|
|
// target's stored config blob holds a credential, and it
|
|
// must never reach a template.
|
|
"Entrypoints": entrypointViews,
|
|
"Targets": h.targetRows(&webhook, targets),
|
|
"Events": events,
|
|
"BaseURL": baseURL,
|
|
"Stats": h.loadWebhookStats(webhook.ID, entrypoints, targets),
|
|
tmplKeyTargetForm: targetForm,
|
|
"TargetError": targetErr,
|
|
// The add target form's selects start on its expiry and
|
|
// rotation through Alpine, so no choice is selected here.
|
|
tmplKeyArchiveExpiryChoices: archiveExpiryChoices(),
|
|
tmplKeyArchiveRotationChoices: archiveRotationChoices(),
|
|
}
|
|
|
|
status := http.StatusOK
|
|
if targetErr != "" {
|
|
status = http.StatusBadRequest
|
|
}
|
|
|
|
h.renderTemplateStatus(w, r, "source_detail.html", data, status)
|
|
}
|