Split source_management.go along its CRUD seams (closes #274)
check / check (push) Successful in 3m22s
check / check (push) Successful in 3m22s
`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
This commit was merged in pull request #494.
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user