Add user webhooks

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-02-24 14:07:36 +01:00
parent 3a294cbaa1
commit 6905472fba
10 changed files with 108 additions and 8 deletions

View File

@@ -0,0 +1,3 @@
ALTER TYPE webhook_event_type ADD VALUE 'user:created';
ALTER TYPE webhook_event_type ADD VALUE 'user:updated';
ALTER TYPE webhook_event_type ADD VALUE 'user:deleted';

View File

@@ -29,6 +29,9 @@ const (
WebhookEventTypeVendorCreated WebhookEventType = "vendor:created"
WebhookEventTypeVendorUpdated WebhookEventType = "vendor:updated"
WebhookEventTypeVendorDeleted WebhookEventType = "vendor:deleted"
WebhookEventTypeUserCreated WebhookEventType = "user:created"
WebhookEventTypeUserUpdated WebhookEventType = "user:updated"
WebhookEventTypeUserDeleted WebhookEventType = "user:deleted"
)
func (w WebhookEventType) String() string {
@@ -38,7 +41,8 @@ func (w WebhookEventType) String() string {
func (w WebhookEventType) IsValid() bool {
switch w {
case WebhookEventTypeMeetingCreated, WebhookEventTypeMeetingUpdated, WebhookEventTypeMeetingDeleted,
WebhookEventTypeVendorCreated, WebhookEventTypeVendorUpdated, WebhookEventTypeVendorDeleted:
WebhookEventTypeVendorCreated, WebhookEventTypeVendorUpdated, WebhookEventTypeVendorDeleted,
WebhookEventTypeUserCreated, WebhookEventTypeUserUpdated, WebhookEventTypeUserDeleted:
return true
}
return false

View File

@@ -33,6 +33,8 @@ import (
"go.probo.inc/probo/pkg/slug"
"go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/validator"
"go.probo.inc/probo/pkg/webhook"
webhooktypes "go.probo.inc/probo/pkg/webhook/types"
)
type (
@@ -320,6 +322,10 @@ func (s *OrganizationService) RemoveUser(
}
}
if err := webhook.InsertData(ctx, tx, scope, organizationID, coredata.WebhookEventTypeUserDeleted, webhooktypes.NewUser(&profile)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
if err := profile.Delete(ctx, tx, scope, profileID); err != nil {
return fmt.Errorf("cannot delete profile: %w", err)
}
@@ -920,6 +926,10 @@ func (s *OrganizationService) CreateUser(ctx context.Context, req *CreateUserReq
return fmt.Errorf("cannot insert membership: %w", err)
}
if err := webhook.InsertData(ctx, conn, scope, req.OrganizationID, coredata.WebhookEventTypeUserCreated, webhooktypes.NewUser(profile)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
},
)
@@ -941,7 +951,7 @@ func (s *OrganizationService) UpdateUser(ctx context.Context, req *UpdateUserReq
profile = &coredata.MembershipProfile{}
)
err := s.pg.WithConn(
err := s.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := profile.LoadByID(ctx, conn, scope, req.ID); err != nil {
@@ -967,6 +977,10 @@ func (s *OrganizationService) UpdateUser(ctx context.Context, req *UpdateUserReq
return fmt.Errorf("cannot update profile: %w", err)
}
if err := webhook.InsertData(ctx, conn, scope, profile.OrganizationID, coredata.WebhookEventTypeUserUpdated, webhooktypes.NewUser(profile)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
},
)

View File

@@ -39,6 +39,8 @@ import (
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/webhook"
webhooktypes "go.probo.inc/probo/pkg/webhook/types"
)
type (
@@ -174,6 +176,7 @@ func (s *Service) CreateUser(
}
// Check if profile exists
eventType := coredata.WebhookEventTypeUserUpdated
profile = &coredata.MembershipProfile{}
if err := profile.LoadByIdentityIDAndOrganizationID(
ctx,
@@ -187,6 +190,7 @@ func (s *Service) CreateUser(
ID: gid.New(config.OrganizationID.TenantID(), coredata.MembershipProfileEntityType),
IdentityID: identity.ID,
OrganizationID: config.OrganizationID,
EmailAddress: emailAddr,
Source: coredata.ProfileSourceSCIM,
State: profileState,
FullName: fullName,
@@ -199,6 +203,7 @@ func (s *Service) CreateUser(
if err != nil {
return fmt.Errorf("cannot insert profile: %w", err)
}
eventType = coredata.WebhookEventTypeUserCreated
} else {
return fmt.Errorf("cannot load profile: %w", err)
}
@@ -257,6 +262,10 @@ func (s *Service) CreateUser(
}
}
if err := webhook.InsertData(ctx, tx, scope, config.OrganizationID, eventType, webhooktypes.NewUser(profile)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
})
@@ -509,6 +518,10 @@ func (s *Service) updateUser(
}
}
if err := webhook.InsertData(ctx, tx, scope, config.OrganizationID, coredata.WebhookEventTypeUserUpdated, webhooktypes.NewUser(profile)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
},
)

View File

@@ -2282,6 +2282,12 @@ enum WebhookEventType
@goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeVendorUpdated")
VENDOR_DELETED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeVendorDeleted")
USER_CREATED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeUserCreated")
USER_UPDATED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeUserUpdated")
USER_DELETED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeUserDeleted")
}
type WebhookSubscription implements Node {

57
pkg/webhook/types/user.go Normal file
View File

@@ -0,0 +1,57 @@
// 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 types
import (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
)
type User struct {
ID gid.GID `json:"id"`
OrganizationID gid.GID `json:"organizationId"`
EmailAddress mail.Addr `json:"emailAddress"`
FullName string `json:"fullName"`
Kind coredata.MembershipProfileKind `json:"kind"`
Source coredata.ProfileSource `json:"source"`
State coredata.ProfileState `json:"state"`
AdditionalEmailAddresses mail.Addrs `json:"additionalEmailAddresses"`
Position *string `json:"position"`
ContractStartDate *time.Time `json:"contractStartDate"`
ContractEndDate *time.Time `json:"contractEndDate"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func NewUser(p *coredata.MembershipProfile) *User {
return &User{
ID: p.ID,
OrganizationID: p.OrganizationID,
EmailAddress: p.EmailAddress,
FullName: p.FullName,
Kind: p.Kind,
Source: p.Source,
State: p.State,
AdditionalEmailAddresses: p.AdditionalEmailAddresses,
Position: p.Position,
ContractStartDate: p.ContractStartDate,
ContractEndDate: p.ContractEndDate,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
}