feat: add entrypoint/target management controls (closes #25)

Add toggle (activate/deactivate) and delete buttons for individual
entrypoints and targets on the webhook detail page. Each action is a
POST form submission with ownership verification.

New routes:
  POST /source/{id}/entrypoints/{entrypointID}/delete
  POST /source/{id}/entrypoints/{entrypointID}/toggle
  POST /source/{id}/targets/{targetID}/delete
  POST /source/{id}/targets/{targetID}/toggle
This commit is contained in:
clawbot
2026-03-01 16:38:14 -08:00
parent 2606d41c60
commit f21a007a3c
3 changed files with 167 additions and 7 deletions

View File

@@ -527,6 +527,144 @@ func (h *Handlers) HandleTargetCreate() http.HandlerFunc {
}
}
// HandleEntrypointDelete handles deleting an individual entrypoint.
func (h *Handlers) HandleEntrypointDelete() 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
}
sourceID := chi.URLParam(r, "sourceID")
entrypointID := chi.URLParam(r, "entrypointID")
// Verify webhook ownership
var webhook database.Webhook
if err := h.db.DB().Where("id = ? AND user_id = ?", sourceID, userID).First(&webhook).Error; err != nil {
http.NotFound(w, r)
return
}
// Delete entrypoint (must belong to this webhook)
result := h.db.DB().Where("id = ? AND webhook_id = ?", entrypointID, webhook.ID).Delete(&database.Entrypoint{})
if result.Error != nil {
h.log.Error("failed to delete entrypoint", "error", result.Error)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/source/"+webhook.ID, http.StatusSeeOther)
}
}
// HandleEntrypointToggle handles toggling the active state of an entrypoint.
func (h *Handlers) HandleEntrypointToggle() 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
}
sourceID := chi.URLParam(r, "sourceID")
entrypointID := chi.URLParam(r, "entrypointID")
// Verify webhook ownership
var webhook database.Webhook
if err := h.db.DB().Where("id = ? AND user_id = ?", sourceID, userID).First(&webhook).Error; err != nil {
http.NotFound(w, r)
return
}
// Find the entrypoint
var entrypoint database.Entrypoint
if err := h.db.DB().Where("id = ? AND webhook_id = ?", entrypointID, webhook.ID).First(&entrypoint).Error; err != nil {
http.NotFound(w, r)
return
}
// Toggle active state
entrypoint.Active = !entrypoint.Active
if err := h.db.DB().Save(&entrypoint).Error; err != nil {
h.log.Error("failed to toggle entrypoint", "error", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/source/"+webhook.ID, http.StatusSeeOther)
}
}
// HandleTargetDelete handles deleting an individual target.
func (h *Handlers) HandleTargetDelete() 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
}
sourceID := chi.URLParam(r, "sourceID")
targetID := chi.URLParam(r, "targetID")
// Verify webhook ownership
var webhook database.Webhook
if err := h.db.DB().Where("id = ? AND user_id = ?", sourceID, userID).First(&webhook).Error; err != nil {
http.NotFound(w, r)
return
}
// Delete target (must belong to this webhook)
result := h.db.DB().Where("id = ? AND webhook_id = ?", targetID, webhook.ID).Delete(&database.Target{})
if result.Error != nil {
h.log.Error("failed to delete target", "error", result.Error)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/source/"+webhook.ID, http.StatusSeeOther)
}
}
// HandleTargetToggle handles toggling the active state of a target.
func (h *Handlers) HandleTargetToggle() 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
}
sourceID := chi.URLParam(r, "sourceID")
targetID := chi.URLParam(r, "targetID")
// Verify webhook ownership
var webhook database.Webhook
if err := h.db.DB().Where("id = ? AND user_id = ?", sourceID, userID).First(&webhook).Error; err != nil {
http.NotFound(w, r)
return
}
// Find the target
var target database.Target
if err := h.db.DB().Where("id = ? AND webhook_id = ?", targetID, webhook.ID).First(&target).Error; err != nil {
http.NotFound(w, r)
return
}
// Toggle active state
target.Active = !target.Active
if err := h.db.DB().Save(&target).Error; err != nil {
h.log.Error("failed to toggle target", "error", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/source/"+webhook.ID, http.StatusSeeOther)
}
}
// getUserID extracts the user ID from the session.
func (h *Handlers) getUserID(r *http.Request) (string, bool) {
sess, err := h.session.Get(r)

View File

@@ -105,8 +105,12 @@ func (s *Server) SetupRoutes() {
r.Post("/edit", s.h.HandleSourceEditSubmit()) // Handle edit submission
r.Post("/delete", s.h.HandleSourceDelete()) // Delete webhook
r.Get("/logs", s.h.HandleSourceLogs()) // View webhook logs
r.Post("/entrypoints", s.h.HandleEntrypointCreate()) // Add entrypoint
r.Post("/targets", s.h.HandleTargetCreate()) // Add target
r.Post("/entrypoints", s.h.HandleEntrypointCreate()) // Add entrypoint
r.Post("/entrypoints/{entrypointID}/delete", s.h.HandleEntrypointDelete()) // Delete entrypoint
r.Post("/entrypoints/{entrypointID}/toggle", s.h.HandleEntrypointToggle()) // Toggle entrypoint active
r.Post("/targets", s.h.HandleTargetCreate()) // Add target
r.Post("/targets/{targetID}/delete", s.h.HandleTargetDelete()) // Delete target
r.Post("/targets/{targetID}/toggle", s.h.HandleTargetToggle()) // Toggle target active
})
// Entrypoint endpoint — accepts incoming webhook POST requests only.