Files
hanko/backend/handler/webhook.go
2025-09-25 19:15:20 +02:00

256 lines
6.2 KiB
Go

package handler
import (
"fmt"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/v2/config"
"github.com/teamhanko/hanko/backend/v2/dto/admin"
"github.com/teamhanko/hanko/backend/v2/persistence"
"github.com/teamhanko/hanko/backend/v2/persistence/models"
"github.com/teamhanko/hanko/backend/v2/webhooks"
"github.com/teamhanko/hanko/backend/v2/webhooks/events"
"net/http"
"time"
)
type WebhookHandler interface {
List(ctx echo.Context) error
Create(ctx echo.Context) error
Get(ctx echo.Context) error
Delete(ctx echo.Context) error
Update(ctx echo.Context) error
}
const (
uuidErrorFormat = "unable to create uuid: %w"
)
type webhookHandler struct {
cfg config.WebhookSettings
persister persistence.Persister
}
func NewWebhookHandler(cfg config.WebhookSettings, persister persistence.Persister) WebhookHandler {
return &webhookHandler{
cfg: cfg,
persister: persister,
}
}
func (w *webhookHandler) List(ctx echo.Context) error {
persister := w.persister.GetWebhookPersister(nil)
dbHooks, err := persister.List(true)
if err != nil {
ctx.Logger().Error(err)
return fmt.Errorf("failed to list users: %w", err)
}
listDto := admin.WebhookListResponseDto{
Database: dbHooks,
Config: w.cfg.Hooks,
}
return ctx.JSON(http.StatusOK, listDto)
}
func (w *webhookHandler) Create(ctx echo.Context) error {
var dto admin.CreateWebhookRequestDto
err := ctx.Bind(&dto)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
err = ctx.Validate(dto)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
now := time.Now()
newUuid, err := uuid.NewV4()
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf(uuidErrorFormat, err))
}
model := models.Webhook{
ID: newUuid,
Callback: dto.Callback,
Enabled: true,
Failures: 0,
ExpiresAt: now.Add(webhooks.WebhookExpireDuration), // 30 Days from now
WebhookEvents: nil,
CreatedAt: now,
UpdatedAt: now,
}
dbEvents, err := w.createWebhookEvents(dto.Events, model, now)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf(uuidErrorFormat, err))
}
persister := w.persister.GetWebhookPersister(nil)
err = persister.Create(model, dbEvents)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("unable to save webhook: %w", err))
}
model.WebhookEvents = dbEvents
return ctx.JSON(http.StatusCreated, model)
}
func (w *webhookHandler) createWebhookEvents(evts events.Events, webhook models.Webhook, now time.Time) (models.WebhookEvents, error) {
eventList := make(models.WebhookEvents, 0)
for _, event := range evts {
newUuid, err := uuid.NewV4()
if err != nil {
return eventList, err
}
model := models.WebhookEvent{
ID: newUuid,
Webhook: &webhook,
Event: string(event),
CreatedAt: now,
UpdatedAt: now,
}
eventList = append(eventList, model)
}
return eventList, nil
}
func (w *webhookHandler) Get(ctx echo.Context) error {
var dto admin.GetWebhookRequestDto
err := ctx.Bind(&dto)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
err = ctx.Validate(dto)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
webhookId, _ := uuid.FromString(dto.ID)
webhook, err := w.getWebhook(webhookId, w.persister.GetWebhookPersister(nil))
if err != nil {
ctx.Logger().Error(err)
return err
}
return ctx.JSON(http.StatusOK, webhook)
}
func (w *webhookHandler) Delete(ctx echo.Context) error {
var dto admin.GetWebhookRequestDto
err := ctx.Bind(&dto)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
err = ctx.Validate(dto)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
persister := w.persister.GetWebhookPersister(nil)
webhookId, _ := uuid.FromString(dto.ID)
webhook, err := w.getWebhook(webhookId, persister)
if err != nil {
ctx.Logger().Error(err)
return err
}
err = persister.Delete(*webhook)
if err != nil {
ctx.Logger().Error(err)
return fmt.Errorf("unable to delete webhook from database: %w", err)
}
return ctx.NoContent(http.StatusNoContent)
}
func (w *webhookHandler) Update(ctx echo.Context) error {
var dto admin.UpdateWebhookRequestDto
err := ctx.Bind(&dto)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
err = ctx.Validate(dto)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
return w.persister.Transaction(func(tx *pop.Connection) error {
persister := w.persister.GetWebhookPersister(tx)
webhookId, _ := uuid.FromString(dto.ID)
webhook, err := w.getWebhook(webhookId, persister)
if err != nil {
ctx.Logger().Error(err)
return err
}
for _, event := range webhook.WebhookEvents {
err := persister.RemoveEvent(event)
if err != nil {
ctx.Logger().Error(err)
return fmt.Errorf("unable to delete event: %w", err)
}
}
now := time.Now()
dbEvents, err := w.createWebhookEvents(dto.Events, *webhook, now)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf(uuidErrorFormat, err))
}
webhook.WebhookEvents = dbEvents
webhook.Callback = dto.Callback
webhook.UpdatedAt = now
webhook.Enabled = dto.Enabled
webhook.Failures = 0
webhook.ExpiresAt = now.Add(webhooks.WebhookExpireDuration)
err = persister.Update(*webhook)
if err != nil {
ctx.Logger().Error(err)
return fmt.Errorf("unable to update webhook: %w", err)
}
return ctx.JSON(http.StatusOK, webhook)
})
}
func (w *webhookHandler) getWebhook(id uuid.UUID, persister persistence.WebhookPersister) (*models.Webhook, error) {
webhook, err := persister.Get(id)
if err != nil {
return nil, fmt.Errorf("unable to fetch webhook from database: %w", err)
}
if webhook == nil {
return nil, echo.NewHTTPError(http.StatusNotFound, "unable to find webhook with id: %s", id.String())
}
return webhook, nil
}