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. // getUserID extracts the user ID from the session.
func (h *Handlers) getUserID(r *http.Request) (string, bool) { func (h *Handlers) getUserID(r *http.Request) (string, bool) {
sess, err := h.session.Get(r) 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("/edit", s.h.HandleSourceEditSubmit()) // Handle edit submission
r.Post("/delete", s.h.HandleSourceDelete()) // Delete webhook r.Post("/delete", s.h.HandleSourceDelete()) // Delete webhook
r.Get("/logs", s.h.HandleSourceLogs()) // View webhook logs r.Get("/logs", s.h.HandleSourceLogs()) // View webhook logs
r.Post("/entrypoints", s.h.HandleEntrypointCreate()) // Add entrypoint r.Post("/entrypoints", s.h.HandleEntrypointCreate()) // Add entrypoint
r.Post("/targets", s.h.HandleTargetCreate()) // Add target 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. // Entrypoint endpoint — accepts incoming webhook POST requests only.

View File

@@ -49,11 +49,21 @@
<div class="p-4"> <div class="p-4">
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<span class="text-sm font-medium text-gray-900">{{if .Description}}{{.Description}}{{else}}Entrypoint{{end}}</span> <span class="text-sm font-medium text-gray-900">{{if .Description}}{{.Description}}{{else}}Entrypoint{{end}}</span>
{{if .Active}} <div class="flex items-center gap-2">
<span class="badge-success">Active</span> {{if .Active}}
{{else}} <span class="badge-success">Active</span>
<span class="badge-error">Inactive</span> {{else}}
{{end}} <span class="badge-error">Inactive</span>
{{end}}
<form method="POST" action="/source/{{$.Webhook.ID}}/entrypoints/{{.ID}}/toggle" class="inline">
<button type="submit" class="text-xs text-gray-500 hover:text-primary-600" title="{{if .Active}}Deactivate{{else}}Activate{{end}}">
{{if .Active}}Deactivate{{else}}Activate{{end}}
</button>
</form>
<form method="POST" action="/source/{{$.Webhook.ID}}/entrypoints/{{.ID}}/delete" onsubmit="return confirm('Delete this entrypoint?')" class="inline">
<button type="submit" class="text-xs text-red-500 hover:text-red-700" title="Delete">Delete</button>
</form>
</div>
</div> </div>
<code class="text-xs text-gray-500 break-all block mt-1">{{$.BaseURL}}/webhook/{{.Path}}</code> <code class="text-xs text-gray-500 break-all block mt-1">{{$.BaseURL}}/webhook/{{.Path}}</code>
</div> </div>
@@ -110,6 +120,14 @@
{{else}} {{else}}
<span class="badge-error">Inactive</span> <span class="badge-error">Inactive</span>
{{end}} {{end}}
<form method="POST" action="/source/{{$.Webhook.ID}}/targets/{{.ID}}/toggle" class="inline">
<button type="submit" class="text-xs text-gray-500 hover:text-primary-600" title="{{if .Active}}Deactivate{{else}}Activate{{end}}">
{{if .Active}}Deactivate{{else}}Activate{{end}}
</button>
</form>
<form method="POST" action="/source/{{$.Webhook.ID}}/targets/{{.ID}}/delete" onsubmit="return confirm('Delete this target?')" class="inline">
<button type="submit" class="text-xs text-red-500 hover:text-red-700" title="Delete">Delete</button>
</form>
</div> </div>
</div> </div>
{{if .Config}} {{if .Config}}