mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 16:01:16 +00:00
170 lines
4.8 KiB
Go
170 lines
4.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// webhookEndpointPayload is the wire shape for create/update requests.
|
|
// The secret is server-generated and only returned at create / rotate
|
|
// time — clients cannot set it directly.
|
|
type webhookEndpointPayload struct {
|
|
URL string `json:"url"`
|
|
Description string `json:"description"`
|
|
EventTypes []string `json:"event_types"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
|
|
// ListWebhookEndpoints returns every endpoint configured for the caller's
|
|
// organization. Secrets are not included.
|
|
func (h *Handler) ListWebhookEndpoints(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
endpoints, err := h.WebhookService.ListEndpoints(c.Request.Context(), orgID)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "failed to list endpoints"))
|
|
return
|
|
}
|
|
if endpoints == nil {
|
|
endpoints = []models.WebhookEndpoint{}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"endpoints": endpoints,
|
|
"event_types": models.AllWebhookEventTypes,
|
|
})
|
|
}
|
|
|
|
// CreateWebhookEndpoint creates a new subscription. The response includes
|
|
// the signing secret — this is the only time it is returned, so clients
|
|
// must capture it. Subsequent reads return the endpoint without the secret.
|
|
func (h *Handler) CreateWebhookEndpoint(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var p webhookEndpointPayload
|
|
if err := c.ShouldBindJSON(&p); err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid payload"))
|
|
return
|
|
}
|
|
enabled := true
|
|
if p.Enabled != nil {
|
|
enabled = *p.Enabled
|
|
}
|
|
endpoint, err := h.WebhookService.CreateEndpoint(c.Request.Context(), orgID, p.URL, p.Description, p.EventTypes, enabled)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, err.Error()))
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, endpoint)
|
|
}
|
|
|
|
// UpdateWebhookEndpoint updates a subscription. The secret is not changed
|
|
// here — use POST /webhooks/:id/rotate-secret to rotate it.
|
|
func (h *Handler) UpdateWebhookEndpoint(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
endpointID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid endpoint id"))
|
|
return
|
|
}
|
|
var p webhookEndpointPayload
|
|
if err := c.ShouldBindJSON(&p); err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid payload"))
|
|
return
|
|
}
|
|
enabled := true
|
|
if p.Enabled != nil {
|
|
enabled = *p.Enabled
|
|
}
|
|
endpoint, err := h.WebhookService.UpdateEndpoint(c.Request.Context(), orgID, endpointID, p.URL, p.Description, p.EventTypes, enabled)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, err.Error()))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, endpoint)
|
|
}
|
|
|
|
// DeleteWebhookEndpoint deletes a subscription and cascades to its
|
|
// delivery history.
|
|
func (h *Handler) DeleteWebhookEndpoint(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
endpointID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid endpoint id"))
|
|
return
|
|
}
|
|
if err := h.WebhookService.DeleteEndpoint(c.Request.Context(), orgID, endpointID); err != nil {
|
|
errx.JSON(c, errx.New(errx.NotFound, err.Error()))
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// RotateWebhookSecret issues a new signing secret and returns it once.
|
|
// Existing in-flight deliveries continue to use the old secret until they
|
|
// settle; new deliveries use the new one.
|
|
func (h *Handler) RotateWebhookSecret(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
endpointID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid endpoint id"))
|
|
return
|
|
}
|
|
secret, err := h.WebhookService.RotateSecret(c.Request.Context(), orgID, endpointID)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.NotFound, err.Error()))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"secret": secret})
|
|
}
|
|
|
|
// ListWebhookDeliveries returns recent delivery attempts for an endpoint.
|
|
// Useful for the dashboard and for debugging integration failures.
|
|
func (h *Handler) ListWebhookDeliveries(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
endpointID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid endpoint id"))
|
|
return
|
|
}
|
|
limit := 50
|
|
if raw := c.Query("limit"); raw != "" {
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil || n <= 0 || n > 200 {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "limit must be between 1 and 200"))
|
|
return
|
|
}
|
|
limit = n
|
|
}
|
|
deliveries, err := h.WebhookService.ListDeliveries(c.Request.Context(), orgID, endpointID, limit)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "failed to list deliveries"))
|
|
return
|
|
}
|
|
if deliveries == nil {
|
|
deliveries = []models.WebhookDelivery{}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"deliveries": deliveries})
|
|
}
|