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 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) }