Files
catalyst/app/webhook/webhooks.go
2025-09-21 17:26:18 +02:00

140 lines
4.2 KiB
Go

package webhook
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"github.com/SecurityBrewery/catalyst/app/auth/usercontext"
"github.com/SecurityBrewery/catalyst/app/database"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
"github.com/SecurityBrewery/catalyst/app/hook"
)
type Webhook struct {
ID string `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Collection string `db:"collection" json:"collection"`
Destination string `db:"destination" json:"destination"`
}
func BindHooks(hooks *hook.Hooks, queries *sqlc.Queries) {
hooks.OnRecordAfterCreateRequest.Subscribe(func(ctx context.Context, table string, record any) {
event(ctx, queries, database.CreateAction, table, record)
})
hooks.OnRecordAfterUpdateRequest.Subscribe(func(ctx context.Context, table string, record any) {
event(ctx, queries, database.UpdateAction, table, record)
})
hooks.OnRecordAfterDeleteRequest.Subscribe(func(ctx context.Context, table string, record any) {
event(ctx, queries, database.DeleteAction, table, record)
})
}
type Payload struct {
Action string `json:"action"`
Collection string `json:"collection"`
Record any `json:"record"`
Auth *AuthUser `json:"auth,omitempty"`
Admin *AuthUser `json:"admin,omitempty"`
}
type AuthUser struct {
ID string `json:"id"`
Username string `json:"username"`
Active bool `json:"active"`
Name *string `json:"name,omitempty"`
Email *string `json:"email,omitempty"`
Avatar *string `json:"avatar,omitempty"`
Lastresetsentat *time.Time `json:"lastresetsentat,omitempty"`
Lastverificationsentat *time.Time `json:"lastverificationsentat,omitempty"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
func SanitizeUser(user *sqlc.User) *AuthUser {
if user == nil {
return nil
}
return &AuthUser{
ID: user.ID,
Username: user.Username,
Active: user.Active,
Name: user.Name,
Email: user.Email,
Avatar: user.Avatar,
Lastresetsentat: user.Lastresetsentat,
Lastverificationsentat: user.Lastverificationsentat,
Created: user.Created,
Updated: user.Updated,
}
}
func event(ctx context.Context, queries *sqlc.Queries, event, collection string, record any) {
user, ok := usercontext.UserFromContext(ctx)
if !ok {
slog.ErrorContext(ctx, "failed to get auth session")
return
}
webhooks, err := database.PaginateItems(ctx, func(ctx context.Context, offset, limit int64) ([]sqlc.ListWebhooksRow, error) {
return queries.ListWebhooks(ctx, sqlc.ListWebhooksParams{Limit: limit, Offset: offset})
})
if err != nil {
slog.ErrorContext(ctx, "failed to list webhooks", "error", err.Error())
return
}
if len(webhooks) == 0 {
return
}
payload, err := json.Marshal(&Payload{
Action: event,
Collection: collection,
Record: record,
Auth: SanitizeUser(user),
Admin: nil,
})
if err != nil {
slog.ErrorContext(ctx, "failed to marshal payload", "error", err.Error())
return
}
for _, webhook := range webhooks {
if err := sendWebhook(ctx, webhook, payload); err != nil {
slog.ErrorContext(ctx, "failed to send webhook", "action", event, "name", webhook.Name, "collection", webhook.Collection, "destination", webhook.Destination, "error", err.Error())
} else {
slog.InfoContext(ctx, "webhook sent", "action", event, "name", webhook.Name, "collection", webhook.Collection, "destination", webhook.Destination)
}
}
}
func sendWebhook(ctx context.Context, webhook sqlc.ListWebhooksRow, payload []byte) error {
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, webhook.Destination, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to send webhook: %s", string(b))
}
return nil
}