feat: webhooker 1.0 MVP — entity rename, core engine, delivery, management UI #16

Merged
sneak merged 33 commits from feature/mvp-1.0 into main 2026-03-04 01:19:41 +01:00
10 changed files with 63 additions and 63 deletions
Showing only changes of commit 7bbe47b943 - Show all commits

View File

@@ -0,0 +1,14 @@
package database
// Entrypoint represents an inbound URL endpoint that feeds into a webhook
type Entrypoint struct {
BaseModel
WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"`
Path string `gorm:"uniqueIndex;not null" json:"path"` // URL path for this entrypoint
Description string `json:"description"`
Active bool `gorm:"default:true" json:"active"`
// Relations
Webhook Webhook `json:"webhook,omitempty"`
}

View File

@@ -1,11 +1,11 @@
package database package database
// Event represents a webhook event // Event represents a captured webhook event
type Event struct { type Event struct {
BaseModel BaseModel
ProcessorID string `gorm:"type:uuid;not null" json:"processor_id"` WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"`
WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"` EntrypointID string `gorm:"type:uuid;not null" json:"entrypoint_id"`
// Request data // Request data
Method string `gorm:"not null" json:"method"` Method string `gorm:"not null" json:"method"`
@@ -14,7 +14,7 @@ type Event struct {
ContentType string `json:"content_type"` ContentType string `json:"content_type"`
// Relations // Relations
Processor Processor `json:"processor,omitempty"`
Webhook Webhook `json:"webhook,omitempty"` Webhook Webhook `json:"webhook,omitempty"`
Entrypoint Entrypoint `json:"entrypoint,omitempty"`
Deliveries []Delivery `json:"deliveries,omitempty"` Deliveries []Delivery `json:"deliveries,omitempty"`
} }

View File

@@ -1,16 +0,0 @@
package database
// Processor represents an event processor
type Processor struct {
BaseModel
UserID string `gorm:"type:uuid;not null" json:"user_id"`
Name string `gorm:"not null" json:"name"`
Description string `json:"description"`
RetentionDays int `gorm:"default:30" json:"retention_days"` // Days to retain events
// Relations
User User `json:"user,omitempty"`
Webhooks []Webhook `json:"webhooks,omitempty"`
Targets []Target `json:"targets,omitempty"`
}

View File

@@ -10,14 +10,14 @@ const (
TargetTypeLog TargetType = "log" TargetTypeLog TargetType = "log"
) )
// Target represents a delivery target for a processor // Target represents a delivery target for a webhook
type Target struct { type Target struct {
BaseModel BaseModel
ProcessorID string `gorm:"type:uuid;not null" json:"processor_id"` WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"`
Name string `gorm:"not null" json:"name"` Name string `gorm:"not null" json:"name"`
Type TargetType `gorm:"not null" json:"type"` Type TargetType `gorm:"not null" json:"type"`
Active bool `gorm:"default:true" json:"active"` Active bool `gorm:"default:true" json:"active"`
// Configuration fields (JSON stored based on type) // Configuration fields (JSON stored based on type)
Config string `gorm:"type:text" json:"config"` // JSON configuration Config string `gorm:"type:text" json:"config"` // JSON configuration
@@ -27,6 +27,6 @@ type Target struct {
MaxQueueSize int `json:"max_queue_size,omitempty"` MaxQueueSize int `json:"max_queue_size,omitempty"`
// Relations // Relations
Processor Processor `json:"processor,omitempty"` Webhook Webhook `json:"webhook,omitempty"`
Deliveries []Delivery `json:"deliveries,omitempty"` Deliveries []Delivery `json:"deliveries,omitempty"`
} }

View File

@@ -8,6 +8,6 @@ type User struct {
Password string `gorm:"not null" json:"-"` // Argon2 hashed Password string `gorm:"not null" json:"-"` // Argon2 hashed
// Relations // Relations
Processors []Processor `json:"processors,omitempty"` Webhooks []Webhook `json:"webhooks,omitempty"`
APIKeys []APIKey `json:"api_keys,omitempty"` APIKeys []APIKey `json:"api_keys,omitempty"`
} }

View File

@@ -1,14 +1,16 @@
package database package database
// Webhook represents a webhook endpoint that feeds into a processor // Webhook represents a webhook processing unit that groups entrypoints and targets
type Webhook struct { type Webhook struct {
BaseModel BaseModel
ProcessorID string `gorm:"type:uuid;not null" json:"processor_id"` UserID string `gorm:"type:uuid;not null" json:"user_id"`
Path string `gorm:"uniqueIndex;not null" json:"path"` // URL path for this webhook Name string `gorm:"not null" json:"name"`
Description string `json:"description"` Description string `json:"description"`
Active bool `gorm:"default:true" json:"active"` RetentionDays int `gorm:"default:30" json:"retention_days"` // Days to retain events
// Relations // Relations
Processor Processor `json:"processor,omitempty"` User User `json:"user,omitempty"`
Entrypoints []Entrypoint `json:"entrypoints,omitempty"`
Targets []Target `json:"targets,omitempty"`
} }

View File

@@ -5,8 +5,8 @@ func (d *Database) Migrate() error {
return d.db.AutoMigrate( return d.db.AutoMigrate(
&User{}, &User{},
&APIKey{}, &APIKey{},
&Processor{},
&Webhook{}, &Webhook{},
&Entrypoint{},
&Target{}, &Target{},
&Event{}, &Event{},
&Delivery{}, &Delivery{},

View File

@@ -4,66 +4,66 @@ import (
"net/http" "net/http"
) )
// HandleSourceList shows a list of user's webhook sources // HandleSourceList shows a list of user's webhooks
func (h *Handlers) HandleSourceList() http.HandlerFunc { func (h *Handlers) HandleSourceList() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement source list page // TODO: Implement webhook list page
http.Error(w, "Not implemented", http.StatusNotImplemented) http.Error(w, "Not implemented", http.StatusNotImplemented)
} }
} }
// HandleSourceCreate shows the form to create a new webhook source // HandleSourceCreate shows the form to create a new webhook
func (h *Handlers) HandleSourceCreate() http.HandlerFunc { func (h *Handlers) HandleSourceCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement source creation form // TODO: Implement webhook creation form
http.Error(w, "Not implemented", http.StatusNotImplemented) http.Error(w, "Not implemented", http.StatusNotImplemented)
} }
} }
// HandleSourceCreateSubmit handles the source creation form submission // HandleSourceCreateSubmit handles the webhook creation form submission
func (h *Handlers) HandleSourceCreateSubmit() http.HandlerFunc { func (h *Handlers) HandleSourceCreateSubmit() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement source creation logic // TODO: Implement webhook creation logic
http.Error(w, "Not implemented", http.StatusNotImplemented) http.Error(w, "Not implemented", http.StatusNotImplemented)
} }
} }
// HandleSourceDetail shows details for a specific webhook source // HandleSourceDetail shows details for a specific webhook
func (h *Handlers) HandleSourceDetail() http.HandlerFunc { func (h *Handlers) HandleSourceDetail() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement source detail page // TODO: Implement webhook detail page
http.Error(w, "Not implemented", http.StatusNotImplemented) http.Error(w, "Not implemented", http.StatusNotImplemented)
} }
} }
// HandleSourceEdit shows the form to edit a webhook source // HandleSourceEdit shows the form to edit a webhook
func (h *Handlers) HandleSourceEdit() http.HandlerFunc { func (h *Handlers) HandleSourceEdit() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement source edit form // TODO: Implement webhook edit form
http.Error(w, "Not implemented", http.StatusNotImplemented) http.Error(w, "Not implemented", http.StatusNotImplemented)
} }
} }
// HandleSourceEditSubmit handles the source edit form submission // HandleSourceEditSubmit handles the webhook edit form submission
func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc { func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement source update logic // TODO: Implement webhook update logic
http.Error(w, "Not implemented", http.StatusNotImplemented) http.Error(w, "Not implemented", http.StatusNotImplemented)
} }
} }
// HandleSourceDelete handles webhook source deletion // HandleSourceDelete handles webhook deletion
func (h *Handlers) HandleSourceDelete() http.HandlerFunc { func (h *Handlers) HandleSourceDelete() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement source deletion logic // TODO: Implement webhook deletion logic
http.Error(w, "Not implemented", http.StatusNotImplemented) http.Error(w, "Not implemented", http.StatusNotImplemented)
} }
} }
// HandleSourceLogs shows the request/response logs for a webhook source // HandleSourceLogs shows the request/response logs for a webhook
func (h *Handlers) HandleSourceLogs() http.HandlerFunc { func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: Implement source logs page // TODO: Implement webhook logs page
http.Error(w, "Not implemented", http.StatusNotImplemented) http.Error(w, "Not implemented", http.StatusNotImplemented)
} }
} }

View File

@@ -6,19 +6,19 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
) )
// HandleWebhook handles incoming webhook requests // HandleWebhook handles incoming webhook requests at entrypoint URLs
func (h *Handlers) HandleWebhook() http.HandlerFunc { func (h *Handlers) HandleWebhook() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// Get webhook UUID from URL // Get entrypoint UUID from URL
webhookUUID := chi.URLParam(r, "uuid") entrypointUUID := chi.URLParam(r, "uuid")
if webhookUUID == "" { if entrypointUUID == "" {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
// Log the incoming webhook request // Log the incoming webhook request
h.log.Info("webhook request received", h.log.Info("webhook request received",
"uuid", webhookUUID, "entrypoint_uuid", entrypointUUID,
"method", r.Method, "method", r.Method,
"remote_addr", r.RemoteAddr, "remote_addr", r.RemoteAddr,
"user_agent", r.UserAgent(), "user_agent", r.UserAgent(),
@@ -32,7 +32,7 @@ func (h *Handlers) HandleWebhook() http.HandlerFunc {
} }
// TODO: Implement webhook handling logic // TODO: Implement webhook handling logic
// For now, return "unimplemented" for all webhook POST requests // Look up entrypoint by UUID, find parent webhook, fan out to targets
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte("unimplemented")) _, err := w.Write([]byte("unimplemented"))
if err != nil { if err != nil {

View File

@@ -90,23 +90,23 @@ func (s *Server) SetupRoutes() {
r.Get("/", s.h.HandleProfile()) r.Get("/", s.h.HandleProfile())
}) })
// Webhook source management routes (require authentication) // Webhook management routes (require authentication)
s.router.Route("/sources", func(r chi.Router) { s.router.Route("/sources", func(r chi.Router) {
// TODO: Add authentication middleware here // TODO: Add authentication middleware here
r.Get("/", s.h.HandleSourceList()) // List all sources r.Get("/", s.h.HandleSourceList()) // List all webhooks
r.Get("/new", s.h.HandleSourceCreate()) // Show create form r.Get("/new", s.h.HandleSourceCreate()) // Show create form
r.Post("/new", s.h.HandleSourceCreateSubmit()) // Handle create submission r.Post("/new", s.h.HandleSourceCreateSubmit()) // Handle create submission
}) })
s.router.Route("/source/{sourceID}", func(r chi.Router) { s.router.Route("/source/{sourceID}", func(r chi.Router) {
// TODO: Add authentication middleware here // TODO: Add authentication middleware here
r.Get("/", s.h.HandleSourceDetail()) // View source details r.Get("/", s.h.HandleSourceDetail()) // View webhook details
r.Get("/edit", s.h.HandleSourceEdit()) // Show edit form r.Get("/edit", s.h.HandleSourceEdit()) // Show edit form
r.Post("/edit", s.h.HandleSourceEditSubmit()) // Handle edit submission r.Post("/edit", s.h.HandleSourceEditSubmit()) // Handle edit submission
r.Post("/delete", s.h.HandleSourceDelete()) // Delete source r.Post("/delete", s.h.HandleSourceDelete()) // Delete webhook
r.Get("/logs", s.h.HandleSourceLogs()) // View source logs r.Get("/logs", s.h.HandleSourceLogs()) // View webhook logs
}) })
// Webhook endpoint - accepts all HTTP methods // Entrypoint endpoint - accepts incoming webhook POST requests
s.router.HandleFunc("/webhook/{uuid}", s.h.HandleWebhook()) s.router.HandleFunc("/webhook/{uuid}", s.h.HandleWebhook())
} }