refactor: rename Processor to Webhook and Webhook to Entrypoint
The top-level entity that groups entrypoints and targets is now called Webhook (was Processor). The inbound URL endpoint entity is now called Entrypoint (was Webhook). This rename affects database models, handler comments, routes, and README documentation. closes #12
This commit is contained in:
25
README.md
25
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
webhooker is a Go web application by [@sneak](https://sneak.berlin) that
|
webhooker is a Go web application by [@sneak](https://sneak.berlin) that
|
||||||
receives, stores, and proxies webhooks to configured targets with retry
|
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
|
## Getting Started
|
||||||
|
|
||||||
@@ -88,15 +88,14 @@ monolithic database:
|
|||||||
- **Main application database** — Stores application configuration and
|
- **Main application database** — Stores application configuration and
|
||||||
all standard webapp data: users, sessions, API keys, and global
|
all standard webapp data: users, sessions, API keys, and global
|
||||||
settings.
|
settings.
|
||||||
- **Per-processor databases** — Each processor (working name — a better
|
- **Per-webhook databases** — Each webhook gets its own dedicated SQLite
|
||||||
term is needed) gets its own dedicated SQLite database file containing:
|
database file containing: input logs, webhook logs, and all output
|
||||||
input logs, processor logs, and all output queues for that specific
|
queues for that specific webhook.
|
||||||
processor.
|
|
||||||
|
|
||||||
This separation provides several benefits: processor databases can be
|
This separation provides several benefits: webhook databases can be
|
||||||
independently backed up, rotated, or archived; a high-volume processor
|
independently backed up, rotated, or archived; a high-volume webhook
|
||||||
won't cause lock contention or bloat affecting the main application; and
|
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.
|
removed.
|
||||||
|
|
||||||
### Package Layout
|
### Package Layout
|
||||||
@@ -120,9 +119,9 @@ The main entry point is `cmd/webhooker/main.go`.
|
|||||||
### Data Model
|
### Data Model
|
||||||
|
|
||||||
- **Users** — Service users with username/password (Argon2id hashing)
|
- **Users** — Service users with username/password (Argon2id hashing)
|
||||||
- **Processors** — Webhook processing units, many-to-one with users
|
- **Webhooks** — Webhook processing units, many-to-one with users
|
||||||
- **Webhooks** — Inbound URL endpoints feeding into processors
|
- **Entrypoints** — Inbound URL endpoints feeding into webhooks
|
||||||
- **Targets** — Delivery destinations per processor (HTTP, retry, database, log)
|
- **Targets** — Delivery destinations per webhook (HTTP, retry, database, log)
|
||||||
- **Events** — Captured webhook payloads
|
- **Events** — Captured webhook payloads
|
||||||
- **Deliveries** — Pairing of events with targets
|
- **Deliveries** — Pairing of events with targets
|
||||||
- **Delivery Results** — Outcome of each delivery attempt
|
- **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)
|
- [ ] Analytics dashboard (success rates, response times)
|
||||||
|
|
||||||
### Phase 5: API
|
### Phase 5: API
|
||||||
- [ ] RESTful CRUD for processors, webhooks, targets
|
- [ ] RESTful CRUD for webhooks, entrypoints, targets
|
||||||
- [ ] Event viewing and filtering endpoints
|
- [ ] Event viewing and filtering endpoints
|
||||||
- [ ] API documentation (OpenAPI)
|
- [ ] API documentation (OpenAPI)
|
||||||
|
|
||||||
@@ -182,7 +181,7 @@ The main entry point is `cmd/webhooker/main.go`.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Pending — to be determined by the author (MIT, GPL, or WTFPL).
|
MIT License. See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
|
|||||||
14
internal/database/model_entrypoint.go
Normal file
14
internal/database/model_entrypoint.go
Normal 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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{},
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user