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
264 lines
6.2 KiB
Go
264 lines
6.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/google/uuid"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// HandleSourceCreate shows the form to create a new webhook.
|
|
func (h *Handlers) HandleSourceCreate() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
h.renderTemplate(
|
|
w, r, "sources_new.html",
|
|
newSourceFormData("", sourceFormInput{
|
|
RetentionDays: strconv.Itoa(
|
|
database.DefaultRetentionDays,
|
|
),
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
// sourceFormInput carries the raw values of the new webhook form. A
|
|
// refused submission is shown again from it, so every value entered
|
|
// comes back, retention included.
|
|
type sourceFormInput struct {
|
|
Name string
|
|
Description string
|
|
RetentionDays string
|
|
// HTTPURL, when not empty, asks for an HTTP target with this
|
|
// destination.
|
|
HTTPURL string
|
|
// Archive asks for a database (archive) target, whose rows expire
|
|
// after ArchiveExpiry and whose files rotate by ArchiveRotation.
|
|
Archive bool
|
|
ArchiveExpiry string
|
|
ArchiveRotation string
|
|
}
|
|
|
|
// newSourceFormData builds the template data for the webhook creation
|
|
// form. It carries the retention default, which the form's help text
|
|
// names, from database.DefaultRetentionDays rather than a hardcoded
|
|
// copy of the same policy.
|
|
func newSourceFormData(
|
|
errMsg string, in sourceFormInput,
|
|
) map[string]any {
|
|
return map[string]any{
|
|
tmplKeyError: errMsg,
|
|
"Form": in,
|
|
"DefaultRetentionDays": database.DefaultRetentionDays,
|
|
tmplKeyArchiveExpiryChoices: archiveExpiryOptions(
|
|
in.ArchiveExpiry,
|
|
),
|
|
tmplKeyArchiveRotationChoices: archiveRotationOptions(
|
|
in.ArchiveRotation,
|
|
),
|
|
}
|
|
}
|
|
|
|
// HandleSourceCreateSubmit handles the webhook creation form
|
|
// submission.
|
|
func (h *Handlers) HandleSourceCreateSubmit() 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
in := sourceFormInput{
|
|
Name: r.PostFormValue("name"),
|
|
Description: r.PostFormValue("description"),
|
|
RetentionDays: r.PostFormValue("retention_days"),
|
|
HTTPURL: r.PostFormValue("http_url"),
|
|
Archive: r.PostFormValue("archive") != "",
|
|
ArchiveExpiry: r.PostFormValue("archive_expiry"),
|
|
ArchiveRotation: r.PostFormValue("archive_rotation"),
|
|
}
|
|
|
|
refuse := func(errMsg string) {
|
|
h.renderTemplateStatus(
|
|
w, r, "sources_new.html",
|
|
newSourceFormData(errMsg, in),
|
|
http.StatusBadRequest,
|
|
)
|
|
}
|
|
|
|
if in.Name == "" {
|
|
refuse("Name is required")
|
|
|
|
return
|
|
}
|
|
|
|
retentionDays, errMsg := parseRetentionDays(
|
|
in.RetentionDays, database.DefaultRetentionDays,
|
|
)
|
|
if errMsg != "" {
|
|
refuse(errMsg)
|
|
|
|
return
|
|
}
|
|
|
|
targets, errMsg, err := h.newWebhookTargets(r.Context(), in)
|
|
if err != nil {
|
|
h.serverError(w, r, "failed to encode target config", err)
|
|
|
|
return
|
|
}
|
|
|
|
if errMsg != "" {
|
|
refuse(errMsg)
|
|
|
|
return
|
|
}
|
|
|
|
h.createWebhookWithEntrypoint(w, r, &database.Webhook{
|
|
UserID: userID,
|
|
Name: in.Name,
|
|
Description: in.Description,
|
|
RetentionDays: retentionDays,
|
|
}, targets)
|
|
}
|
|
}
|
|
|
|
// newWebhookTargets validates the targets the new webhook form asks
|
|
// for and returns the rows to create with the webhook, or the message
|
|
// the form shows for the first one it refuses. A filled-in HTTP URL
|
|
// asks for an HTTP target named "HTTP", and the archive checkbox for a
|
|
// database target named "Archive". Each goes through newTarget, as on
|
|
// the webhook page's add target form. The rows have no WebhookID yet:
|
|
// the webhook has no ID until it is created.
|
|
func (h *Handlers) newWebhookTargets(
|
|
ctx context.Context,
|
|
in sourceFormInput,
|
|
) ([]*database.Target, string, error) {
|
|
var requested []targetFormInput
|
|
|
|
if in.HTTPURL != "" {
|
|
requested = append(requested, targetFormInput{
|
|
Name: "HTTP",
|
|
Type: database.TargetTypeHTTP,
|
|
URL: in.HTTPURL,
|
|
})
|
|
}
|
|
|
|
if in.Archive {
|
|
requested = append(requested, targetFormInput{
|
|
Name: "Archive",
|
|
Type: database.TargetTypeDatabase,
|
|
Expiry: in.ArchiveExpiry,
|
|
Rotation: in.ArchiveRotation,
|
|
})
|
|
}
|
|
|
|
targets := make([]*database.Target, 0, len(requested))
|
|
|
|
for _, form := range requested {
|
|
target, errMsg, err := h.newTarget(ctx, "", form)
|
|
if err != nil || errMsg != "" {
|
|
return nil, errMsg, err
|
|
}
|
|
|
|
targets = append(targets, target)
|
|
}
|
|
|
|
return targets, "", nil
|
|
}
|
|
|
|
// createWebhookWithEntrypoint creates a webhook, its default
|
|
// entrypoint and the given targets in a transaction.
|
|
func (h *Handlers) createWebhookWithEntrypoint(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
webhook *database.Webhook,
|
|
targets []*database.Target,
|
|
) {
|
|
err := h.commitWebhook(webhook, targets)
|
|
if err != nil {
|
|
h.serverError(w, r, "failed to create webhook", err)
|
|
|
|
return
|
|
}
|
|
|
|
err = h.dbMgr.CreateDB(webhook.ID)
|
|
if err != nil {
|
|
h.log.Error(
|
|
"failed to create webhook event database",
|
|
"webhook_id", webhook.ID, "error", err,
|
|
)
|
|
}
|
|
|
|
h.log.Info("webhook created",
|
|
"webhook_id", webhook.ID,
|
|
"name", webhook.Name, "user_id", webhook.UserID,
|
|
)
|
|
|
|
http.Redirect(
|
|
w, r, withNotice("/hook/"+webhook.ID, webhookCreated),
|
|
http.StatusSeeOther,
|
|
)
|
|
}
|
|
|
|
// commitWebhook creates a webhook, its default entrypoint and the
|
|
// given targets in a transaction. Returns an error on failure (rolls
|
|
// back).
|
|
func (h *Handlers) commitWebhook(
|
|
webhook *database.Webhook,
|
|
targets []*database.Target,
|
|
) error {
|
|
tx := h.db.DB().Begin()
|
|
if tx.Error != nil {
|
|
return tx.Error
|
|
}
|
|
|
|
err := tx.Create(webhook).Error
|
|
if err != nil {
|
|
tx.Rollback()
|
|
|
|
return err
|
|
}
|
|
|
|
entrypoint := &database.Entrypoint{
|
|
WebhookID: webhook.ID,
|
|
Path: uuid.New().String(),
|
|
Description: "Default entrypoint",
|
|
Active: true,
|
|
}
|
|
|
|
err = tx.Create(entrypoint).Error
|
|
if err != nil {
|
|
tx.Rollback()
|
|
|
|
return err
|
|
}
|
|
|
|
for _, target := range targets {
|
|
target.WebhookID = webhook.ID
|
|
|
|
err = tx.Create(target).Error
|
|
if err != nil {
|
|
tx.Rollback()
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
return tx.Commit().Error
|
|
}
|