check / check (push) Successful in 3m29s
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, target_delete.go and target_toggle.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. The README and a middleware comment that named the removed file now name the new ones. Model: opus-5-5
231 lines
5.9 KiB
Go
231 lines
5.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// parseRetentionDays interprets a retention_days form value. It
|
|
// returns the number of days, or, for a value it refuses, the message
|
|
// the create and edit forms show; the message is empty when the value
|
|
// is accepted.
|
|
//
|
|
// An empty value yields fallback, which lets the create path apply the
|
|
// default and the edit path leave the stored value unchanged. A value
|
|
// of 0 is returned as 0 and is rewritten to the retain-forever
|
|
// sentinel by database.Webhook's BeforeSave hook. Anything unparseable
|
|
// or negative is refused rather than silently given a default.
|
|
//
|
|
// The upper bound is not cosmetic. The reaper computes its cutoff as a
|
|
// time.Duration, an int64 nanosecond count, so a day count above
|
|
// database.MaxFiniteRetentionDays overflows, puts the cutoff in the
|
|
// future, and deletes every event the webhook has. A finite value
|
|
// above that ceiling is therefore refused, and the message names the
|
|
// ceiling rather than implying the input was not a number.
|
|
//
|
|
// A value at or above the retain-forever sentinel is not out of range:
|
|
// it is what the edit form pre-fills for a retain-forever webhook, so
|
|
// submitting the form back unchanged has to keep meaning "forever"
|
|
// rather than being rejected.
|
|
func parseRetentionDays(raw string, fallback int) (int, string) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return fallback, ""
|
|
}
|
|
|
|
v, err := strconv.Atoi(raw)
|
|
if err != nil || v < 0 {
|
|
return 0, "Retention must be a whole number of days, or 0 to " +
|
|
"retain events forever."
|
|
}
|
|
|
|
if v >= database.RetentionForeverDays {
|
|
return database.RetentionForeverDays, ""
|
|
}
|
|
|
|
if v > database.MaxFiniteRetentionDays {
|
|
return 0, "Retention must be at most " +
|
|
strconv.Itoa(database.MaxFiniteRetentionDays) +
|
|
" days, or 0 to retain events forever."
|
|
}
|
|
|
|
return v, ""
|
|
}
|
|
|
|
// ownedWebhook resolves the request's sourceID parameter to a
|
|
// webhook the session's user owns.
|
|
//
|
|
// Ownership and existence are decided by one query, so a
|
|
// webhook belonging to another user is indistinguishable from
|
|
// one that does not exist: both are a 404, and neither confirms
|
|
// the id. Callers that reach further into a webhook's data —
|
|
// the event log page and the event body download — share this
|
|
// one check rather than restating it, so the download cannot
|
|
// come to authorize differently from the page that links to it.
|
|
//
|
|
// It reports false once it has written the response, which is a
|
|
// redirect to the login page for an unauthenticated request and
|
|
// a 404 otherwise. The caller returns without writing more.
|
|
func (h *Handlers) ownedWebhook(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) (database.Webhook, bool) {
|
|
var webhook database.Webhook
|
|
|
|
userID, ok := h.getUserID(r)
|
|
if !ok {
|
|
http.Redirect(
|
|
w, r, "/pages/login", http.StatusSeeOther,
|
|
)
|
|
|
|
return database.Webhook{}, false
|
|
}
|
|
|
|
sourceID := chi.URLParam(r, "sourceID")
|
|
|
|
err := h.db.DB().Where(
|
|
"id = ? AND user_id = ?", sourceID, userID,
|
|
).First(&webhook).Error
|
|
if err != nil {
|
|
h.renderError(w, r, http.StatusNotFound)
|
|
|
|
return database.Webhook{}, false
|
|
}
|
|
|
|
return webhook, true
|
|
}
|
|
|
|
// deleteChildResource returns a handler that deletes a child
|
|
// resource (entrypoint or target) belonging to a webhook. The
|
|
// optional afterDelete hook runs with the child's id once the
|
|
// delete has removed it, before the redirect, which carries done as
|
|
// its notice.
|
|
func (h *Handlers) deleteChildResource(
|
|
idParam string,
|
|
model any,
|
|
errMsg string,
|
|
afterDelete func(childID string),
|
|
done noticeCode,
|
|
) 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")
|
|
childID := chi.URLParam(r, idParam)
|
|
|
|
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
|
|
}
|
|
|
|
result := h.db.DB().Where(
|
|
"id = ? AND webhook_id = ?",
|
|
childID, webhook.ID,
|
|
).Delete(model)
|
|
if result.Error != nil {
|
|
h.serverError(w, r, errMsg, result.Error)
|
|
|
|
return
|
|
}
|
|
|
|
// Only for a row this webhook really had: the id came from
|
|
// the URL and may name another webhook's child.
|
|
if afterDelete != nil && result.RowsAffected > 0 {
|
|
afterDelete(childID)
|
|
}
|
|
|
|
http.Redirect(
|
|
w, r,
|
|
withNotice("/hook/"+webhook.ID, done),
|
|
http.StatusSeeOther,
|
|
)
|
|
}
|
|
}
|
|
|
|
// toggleChildResource returns a handler that toggles the active
|
|
// state of a child resource belonging to a webhook. toggleFn returns
|
|
// the new state, and the redirect carries activated or deactivated as
|
|
// its notice to match.
|
|
func (h *Handlers) toggleChildResource(
|
|
idParam string,
|
|
toggleFn func(webhookID, childID string) (bool, error),
|
|
errMsg string,
|
|
activated, deactivated noticeCode,
|
|
) 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")
|
|
childID := chi.URLParam(r, idParam)
|
|
|
|
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
|
|
}
|
|
|
|
active, err := toggleFn(webhook.ID, childID)
|
|
if err != nil {
|
|
h.serverError(w, r, errMsg, err)
|
|
|
|
return
|
|
}
|
|
|
|
done := deactivated
|
|
if active {
|
|
done = activated
|
|
}
|
|
|
|
http.Redirect(
|
|
w, r,
|
|
withNotice("/hook/"+webhook.ID, done),
|
|
http.StatusSeeOther,
|
|
)
|
|
}
|
|
}
|
|
|
|
// getUserID extracts the user ID from the session.
|
|
func (h *Handlers) getUserID(
|
|
r *http.Request,
|
|
) (string, bool) {
|
|
sess, err := h.session.Get(r)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
if !h.session.IsAuthenticated(sess) {
|
|
return "", false
|
|
}
|
|
|
|
return h.session.GetUserID(sess)
|
|
}
|