diff --git a/README.md b/README.md index 7b793dc..2298711 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ webhooker is a Go web application by [@sneak](https://sneak.berlin) that receives, stores, and proxies webhooks to configured targets with retry -support, observability, and a management web UI. License: pending. +support, observability, and a management web UI. License: MIT. ## Getting Started @@ -88,15 +88,14 @@ monolithic database: - **Main application database** — Stores application configuration and all standard webapp data: users, sessions, API keys, and global settings. -- **Per-processor databases** — Each processor (working name — a better - term is needed) gets its own dedicated SQLite database file containing: - input logs, processor logs, and all output queues for that specific - processor. +- **Per-webhook databases** — Each webhook gets its own dedicated SQLite + database file containing: input logs, webhook logs, and all output + queues for that specific webhook. -This separation provides several benefits: processor databases can be -independently backed up, rotated, or archived; a high-volume processor +This separation provides several benefits: webhook databases can be +independently backed up, rotated, or archived; a high-volume webhook won't cause lock contention or bloat affecting the main application; and -individual processor data can be cleanly deleted when a processor is +individual webhook data can be cleanly deleted when a webhook is removed. ### Package Layout @@ -120,9 +119,9 @@ The main entry point is `cmd/webhooker/main.go`. ### Data Model - **Users** — Service users with username/password (Argon2id hashing) -- **Processors** — Webhook processing units, many-to-one with users -- **Webhooks** — Inbound URL endpoints feeding into processors -- **Targets** — Delivery destinations per processor (HTTP, retry, database, log) +- **Webhooks** — Webhook processing units, many-to-one with users +- **Entrypoints** — Inbound URL endpoints feeding into webhooks +- **Targets** — Delivery destinations per webhook (HTTP, retry, database, log) - **Events** — Captured webhook payloads - **Deliveries** — Pairing of events with targets - **Delivery Results** — Outcome of each delivery attempt @@ -170,7 +169,7 @@ The main entry point is `cmd/webhooker/main.go`. - [ ] Analytics dashboard (success rates, response times) ### Phase 5: API -- [ ] RESTful CRUD for processors, webhooks, targets +- [ ] RESTful CRUD for webhooks, entrypoints, targets - [ ] Event viewing and filtering endpoints - [ ] API documentation (OpenAPI) @@ -182,7 +181,7 @@ The main entry point is `cmd/webhooker/main.go`. ## License -Pending — to be determined by the author (MIT, GPL, or WTFPL). +MIT License. See [LICENSE](LICENSE) for details. ## Author diff --git a/internal/database/model_entrypoint.go b/internal/database/model_entrypoint.go new file mode 100644 index 0000000..37b7e3b --- /dev/null +++ b/internal/database/model_entrypoint.go @@ -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"` +} diff --git a/internal/database/model_event.go b/internal/database/model_event.go index 7b6ea50..f9dbaed 100644 --- a/internal/database/model_event.go +++ b/internal/database/model_event.go @@ -1,11 +1,11 @@ package database -// Event represents a webhook event +// Event represents a captured webhook event type Event struct { 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 Method string `gorm:"not null" json:"method"` @@ -14,7 +14,7 @@ type Event struct { ContentType string `json:"content_type"` // Relations - Processor Processor `json:"processor,omitempty"` Webhook Webhook `json:"webhook,omitempty"` + Entrypoint Entrypoint `json:"entrypoint,omitempty"` Deliveries []Delivery `json:"deliveries,omitempty"` } diff --git a/internal/database/model_processor.go b/internal/database/model_processor.go deleted file mode 100644 index 121b0c1..0000000 --- a/internal/database/model_processor.go +++ /dev/null @@ -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"` -} diff --git a/internal/database/model_target.go b/internal/database/model_target.go index 76478ba..1c1c842 100644 --- a/internal/database/model_target.go +++ b/internal/database/model_target.go @@ -10,14 +10,14 @@ const ( TargetTypeLog TargetType = "log" ) -// Target represents a delivery target for a processor +// Target represents a delivery target for a webhook type Target struct { BaseModel - ProcessorID string `gorm:"type:uuid;not null" json:"processor_id"` - Name string `gorm:"not null" json:"name"` - Type TargetType `gorm:"not null" json:"type"` - Active bool `gorm:"default:true" json:"active"` + WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"` + Name string `gorm:"not null" json:"name"` + Type TargetType `gorm:"not null" json:"type"` + Active bool `gorm:"default:true" json:"active"` // Configuration fields (JSON stored based on type) Config string `gorm:"type:text" json:"config"` // JSON configuration @@ -27,6 +27,6 @@ type Target struct { MaxQueueSize int `json:"max_queue_size,omitempty"` // Relations - Processor Processor `json:"processor,omitempty"` + Webhook Webhook `json:"webhook,omitempty"` Deliveries []Delivery `json:"deliveries,omitempty"` } diff --git a/internal/database/model_user.go b/internal/database/model_user.go index b31afdb..6a578d0 100644 --- a/internal/database/model_user.go +++ b/internal/database/model_user.go @@ -8,6 +8,6 @@ type User struct { Password string `gorm:"not null" json:"-"` // Argon2 hashed // Relations - Processors []Processor `json:"processors,omitempty"` - APIKeys []APIKey `json:"api_keys,omitempty"` + Webhooks []Webhook `json:"webhooks,omitempty"` + APIKeys []APIKey `json:"api_keys,omitempty"` } diff --git a/internal/database/model_webhook.go b/internal/database/model_webhook.go index 7fae55f..08e4bc4 100644 --- a/internal/database/model_webhook.go +++ b/internal/database/model_webhook.go @@ -1,14 +1,16 @@ 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 { BaseModel - ProcessorID string `gorm:"type:uuid;not null" json:"processor_id"` - Path string `gorm:"uniqueIndex;not null" json:"path"` // URL path for this webhook - Description string `json:"description"` - Active bool `gorm:"default:true" json:"active"` + 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 - Processor Processor `json:"processor,omitempty"` + User User `json:"user,omitempty"` + Entrypoints []Entrypoint `json:"entrypoints,omitempty"` + Targets []Target `json:"targets,omitempty"` } diff --git a/internal/database/models.go b/internal/database/models.go index 560fdce..ce19b36 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -5,8 +5,8 @@ func (d *Database) Migrate() error { return d.db.AutoMigrate( &User{}, &APIKey{}, - &Processor{}, &Webhook{}, + &Entrypoint{}, &Target{}, &Event{}, &Delivery{}, diff --git a/internal/handlers/source_management.go b/internal/handlers/source_management.go index e8cd792..11d166f 100644 --- a/internal/handlers/source_management.go +++ b/internal/handlers/source_management.go @@ -4,66 +4,66 @@ import ( "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 { 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) } } -// 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 { 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) } } -// HandleSourceCreateSubmit handles the source creation form submission +// HandleSourceCreateSubmit handles the webhook creation form submission func (h *Handlers) HandleSourceCreateSubmit() http.HandlerFunc { 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) } } -// HandleSourceDetail shows details for a specific webhook source +// HandleSourceDetail shows details for a specific webhook func (h *Handlers) HandleSourceDetail() http.HandlerFunc { 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) } } -// HandleSourceEdit shows the form to edit a webhook source +// HandleSourceEdit shows the form to edit a webhook func (h *Handlers) HandleSourceEdit() http.HandlerFunc { 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) } } -// HandleSourceEditSubmit handles the source edit form submission +// HandleSourceEditSubmit handles the webhook edit form submission func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc { 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) } } -// HandleSourceDelete handles webhook source deletion +// HandleSourceDelete handles webhook deletion func (h *Handlers) HandleSourceDelete() http.HandlerFunc { 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) } } -// 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 { 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) } } diff --git a/internal/handlers/webhook.go b/internal/handlers/webhook.go index 474d2f8..a515966 100644 --- a/internal/handlers/webhook.go +++ b/internal/handlers/webhook.go @@ -6,19 +6,19 @@ import ( "github.com/go-chi/chi" ) -// HandleWebhook handles incoming webhook requests +// HandleWebhook handles incoming webhook requests at entrypoint URLs func (h *Handlers) HandleWebhook() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // Get webhook UUID from URL - webhookUUID := chi.URLParam(r, "uuid") - if webhookUUID == "" { + // Get entrypoint UUID from URL + entrypointUUID := chi.URLParam(r, "uuid") + if entrypointUUID == "" { http.NotFound(w, r) return } // Log the incoming webhook request h.log.Info("webhook request received", - "uuid", webhookUUID, + "entrypoint_uuid", entrypointUUID, "method", r.Method, "remote_addr", r.RemoteAddr, "user_agent", r.UserAgent(), @@ -32,7 +32,7 @@ func (h *Handlers) HandleWebhook() http.HandlerFunc { } // 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) _, err := w.Write([]byte("unimplemented")) if err != nil { diff --git a/internal/server/routes.go b/internal/server/routes.go index e9bb056..3c177c9 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -90,23 +90,23 @@ func (s *Server) SetupRoutes() { r.Get("/", s.h.HandleProfile()) }) - // Webhook source management routes (require authentication) + // Webhook management routes (require authentication) s.router.Route("/sources", func(r chi.Router) { // 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.Post("/new", s.h.HandleSourceCreateSubmit()) // Handle create submission }) s.router.Route("/source/{sourceID}", func(r chi.Router) { // 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.Post("/edit", s.h.HandleSourceEditSubmit()) // Handle edit submission - r.Post("/delete", s.h.HandleSourceDelete()) // Delete source - r.Get("/logs", s.h.HandleSourceLogs()) // View source logs + r.Post("/delete", s.h.HandleSourceDelete()) // Delete webhook + 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()) }