Add webhooks

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-02-11 09:34:06 +01:00
parent 4945c022a1
commit f9de9c4834
34 changed files with 5932 additions and 4 deletions

View File

@@ -316,4 +316,11 @@ const (
ActionApplicabilityStatementCreate = "core:applicability-statement:create"
ActionApplicabilityStatementUpdate = "core:applicability-statement:update"
ActionApplicabilityStatementDelete = "core:applicability-statement:delete"
// WebhookConfiguration actions
ActionWebhookConfigurationList = "core:webhook-configuration:list"
ActionWebhookConfigurationGet = "core:webhook-configuration:get"
ActionWebhookConfigurationCreate = "core:webhook-configuration:create"
ActionWebhookConfigurationUpdate = "core:webhook-configuration:update"
ActionWebhookConfigurationDelete = "core:webhook-configuration:delete"
)

View File

@@ -23,7 +23,9 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/api/console/v1/types"
"go.probo.inc/probo/pkg/validator"
"go.probo.inc/probo/pkg/webhook"
)
type MeetingService struct {
@@ -207,6 +209,10 @@ func (s MeetingService) Create(
}
}
if err := webhook.InsertEvent(ctx, conn, s.svc.scope, organization.ID, coredata.WebhookEventTypeMeetingCreated, types.NewMeeting(meeting)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
},
)
@@ -283,6 +289,10 @@ func (s MeetingService) Update(
}
}
if err := webhook.InsertEvent(ctx, conn, s.svc.scope, meeting.OrganizationID, coredata.WebhookEventTypeMeetingUpdated, types.NewMeeting(meeting)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
},
)
@@ -307,6 +317,10 @@ func (s MeetingService) Delete(
return fmt.Errorf("cannot load meeting: %w", err)
}
if err := webhook.InsertEvent(ctx, conn, s.svc.scope, meeting.OrganizationID, coredata.WebhookEventTypeMeetingDeleted, types.NewMeeting(meeting)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
if err := meeting.Delete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot delete meeting: %w", err)
}

View File

@@ -79,6 +79,7 @@ var ViewerPolicy = policy.NewPolicy(
ActionRightsRequestGet, ActionRightsRequestList,
ActionStateOfApplicabilityGet, ActionStateOfApplicabilityList,
ActionApplicabilityStatementGet, ActionApplicabilityStatementList,
ActionWebhookConfigurationGet, ActionWebhookConfigurationList,
).WithSID("entity-read-access").When(organizationCondition),
policy.Allow(

View File

@@ -92,6 +92,7 @@ type (
Data *DatumService
Audits *AuditService
Meetings *MeetingService
WebhookConfigurations *WebhookConfigurationService
Reports *ReportService
TrustCenters *TrustCenterService
TrustCenterAccesses *TrustCenterAccessService
@@ -213,6 +214,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
tenantService.Data = &DatumService{svc: tenantService}
tenantService.Audits = &AuditService{svc: tenantService}
tenantService.Meetings = &MeetingService{svc: tenantService}
tenantService.WebhookConfigurations = &WebhookConfigurationService{svc: tenantService}
tenantService.Reports = &ReportService{svc: tenantService}
tenantService.TrustCenters = &TrustCenterService{svc: tenantService}
tenantService.TrustCenterAccesses = &TrustCenterAccessService{svc: tenantService}

View File

@@ -23,7 +23,9 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/api/console/v1/types"
"go.probo.inc/probo/pkg/validator"
"go.probo.inc/probo/pkg/webhook"
)
type (
@@ -392,6 +394,10 @@ func (s VendorService) Update(
return fmt.Errorf("cannot update vendor: %w", err)
}
if err := webhook.InsertEvent(ctx, conn, s.svc.scope, vendor.OrganizationID, coredata.WebhookEventTypeVendorUpdated, types.NewVendor(vendor)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
},
)
@@ -427,10 +433,19 @@ func (s VendorService) Delete(
ctx context.Context,
vendorID gid.GID,
) error {
vendor := coredata.Vendor{ID: vendorID}
return s.svc.pg.WithConn(
vendor := &coredata.Vendor{}
return s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := vendor.LoadByID(ctx, conn, s.svc.scope, vendorID); err != nil {
return fmt.Errorf("cannot load vendor: %w", err)
}
if err := webhook.InsertEvent(ctx, conn, s.svc.scope, vendor.OrganizationID, coredata.WebhookEventTypeVendorDeleted, types.NewVendor(vendor)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return vendor.Delete(ctx, conn, s.svc.scope)
},
)
@@ -504,6 +519,10 @@ func (s VendorService) Create(
return fmt.Errorf("cannot insert vendor: %w", err)
}
if err := webhook.InsertEvent(ctx, conn, s.svc.scope, organization.ID, coredata.WebhookEventTypeVendorCreated, types.NewVendor(vendor)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
},
)

View File

@@ -0,0 +1,290 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package probo
import (
"context"
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
)
type WebhookConfigurationService struct {
svc *TenantService
}
type (
CreateWebhookConfigurationRequest struct {
OrganizationID gid.GID
EndpointURL string
SelectedEvents []coredata.WebhookEventType
}
UpdateWebhookConfigurationRequest struct {
WebhookConfigurationID gid.GID
EndpointURL *string
SelectedEvents []coredata.WebhookEventType
}
)
func (r *CreateWebhookConfigurationRequest) Validate() error {
v := validator.New()
v.Check(r.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(r.EndpointURL, "endpoint_url", validator.Required(), validator.URL())
return v.Error()
}
func (r *UpdateWebhookConfigurationRequest) Validate() error {
v := validator.New()
v.Check(r.WebhookConfigurationID, "webhook_configuration_id", validator.Required(), validator.GID(coredata.WebhookConfigurationEntityType))
v.Check(r.EndpointURL, "endpoint_url", validator.NotEmpty(), validator.URL())
return v.Error()
}
func (s WebhookConfigurationService) ListForOrganizationID(
ctx context.Context,
organizationID gid.GID,
cursor *page.Cursor[coredata.WebhookConfigurationOrderField],
) (*page.Page[*coredata.WebhookConfiguration, coredata.WebhookConfigurationOrderField], error) {
var configurations coredata.WebhookConfigurations
organization := &coredata.Organization{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
err := configurations.LoadByOrganizationID(
ctx,
conn,
s.svc.scope,
organization.ID,
cursor,
)
if err != nil {
return fmt.Errorf("cannot load webhook configurations: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(configurations, cursor), nil
}
func (s WebhookConfigurationService) CountForOrganizationID(
ctx context.Context,
organizationID gid.GID,
) (int, error) {
var count int
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
configurations := &coredata.WebhookConfigurations{}
count, err = configurations.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID)
if err != nil {
return fmt.Errorf("cannot count webhook configurations: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s WebhookConfigurationService) Get(
ctx context.Context,
webhookConfigurationID gid.GID,
) (*coredata.WebhookConfiguration, error) {
wc := &coredata.WebhookConfiguration{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := wc.LoadByID(ctx, conn, s.svc.scope, webhookConfigurationID); err != nil {
return fmt.Errorf("cannot load webhook configuration: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return wc, nil
}
func (s WebhookConfigurationService) Create(
ctx context.Context,
req CreateWebhookConfigurationRequest,
) (*coredata.WebhookConfiguration, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
var wc *coredata.WebhookConfiguration
organization := &coredata.Organization{}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
wc = &coredata.WebhookConfiguration{
ID: gid.New(organization.ID.TenantID(), coredata.WebhookConfigurationEntityType),
OrganizationID: organization.ID,
EndpointURL: req.EndpointURL,
SelectedEvents: req.SelectedEvents,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := wc.GenerateSigningSecret(s.svc.encryptionKey); err != nil {
return fmt.Errorf("cannot generate signing secret: %w", err)
}
if err := wc.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert webhook configuration: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return wc, nil
}
func (s WebhookConfigurationService) Update(
ctx context.Context,
req UpdateWebhookConfigurationRequest,
) (*coredata.WebhookConfiguration, error) {
if err := req.Validate(); err != nil {
return nil, err
}
wc := &coredata.WebhookConfiguration{}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := wc.LoadByID(ctx, conn, s.svc.scope, req.WebhookConfigurationID); err != nil {
return fmt.Errorf("cannot load webhook configuration: %w", err)
}
if req.EndpointURL != nil {
wc.EndpointURL = *req.EndpointURL
}
if req.SelectedEvents != nil {
wc.SelectedEvents = req.SelectedEvents
}
wc.UpdatedAt = time.Now()
if err := wc.Update(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot update webhook configuration: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return wc, nil
}
func (s WebhookConfigurationService) GetSigningSecret(
ctx context.Context,
webhookConfigurationID gid.GID,
) (string, error) {
wc := &coredata.WebhookConfiguration{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := wc.LoadByID(ctx, conn, s.svc.scope, webhookConfigurationID); err != nil {
return fmt.Errorf("cannot load webhook configuration: %w", err)
}
return nil
},
)
if err != nil {
return "", err
}
return wc.DecryptSigningSecret(s.svc.encryptionKey)
}
func (s WebhookConfigurationService) Delete(
ctx context.Context,
webhookConfigurationID gid.GID,
) error {
wc := &coredata.WebhookConfiguration{ID: webhookConfigurationID}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := wc.LoadByID(ctx, conn, s.svc.scope, webhookConfigurationID); err != nil {
return fmt.Errorf("cannot load webhook configuration: %w", err)
}
if err := wc.Delete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot delete webhook configuration: %w", err)
}
return nil
},
)
if err != nil {
return err
}
return nil
}