Add membership and host to user webhook payload

Nest membership ID, role, and state into a membership sub-object
in the user webhook payload. Also emit user:updated webhook when
the membership role is changed. Add X-Probo-Webhook-Host header
to webhook HTTP calls. Skip delete webhook when membership is
not found in SCIM user deletion.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-13 17:00:22 +02:00
parent 0826f28867
commit 06c0972551
5 changed files with 71 additions and 30 deletions

View File

@@ -273,6 +273,15 @@ func (s *OrganizationService) UpdateMempership(
return fmt.Errorf("cannot update membership: %w", err)
}
profile := &coredata.MembershipProfile{}
if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, membership.IdentityID, membership.OrganizationID); err != nil {
return fmt.Errorf("cannot load profile: %w", err)
}
if err := webhook.InsertData(ctx, tx, scope, organizationID, coredata.WebhookEventTypeUserUpdated, webhooktypes.NewUser(profile, &membership)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
return nil
},
); err != nil {
@@ -323,7 +332,7 @@ func (s *OrganizationService) RemoveUser(
}
}
if err := webhook.InsertData(ctx, tx, scope, organizationID, coredata.WebhookEventTypeUserDeleted, webhooktypes.NewUser(&profile)); err != nil {
if err := webhook.InsertData(ctx, tx, scope, organizationID, coredata.WebhookEventTypeUserDeleted, webhooktypes.NewUser(&profile, membership)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
@@ -946,7 +955,7 @@ 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 {
if err := webhook.InsertData(ctx, conn, scope, req.OrganizationID, coredata.WebhookEventTypeUserCreated, webhooktypes.NewUser(profile, membership)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
@@ -999,7 +1008,12 @@ 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 {
membership := &coredata.Membership{}
if err := membership.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, profile.IdentityID, profile.OrganizationID); err != nil {
return fmt.Errorf("cannot load membership: %w", err)
}
if err := webhook.InsertData(ctx, conn, scope, profile.OrganizationID, coredata.WebhookEventTypeUserUpdated, webhooktypes.NewUser(profile, membership)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}

View File

@@ -330,7 +330,7 @@ func (s *Service) CreateUser(
}
}
if err := webhook.InsertData(ctx, tx, scope, config.OrganizationID, eventType, webhooktypes.NewUser(profile)); err != nil {
if err := webhook.InsertData(ctx, tx, scope, config.OrganizationID, eventType, webhooktypes.NewUser(profile, membership)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
@@ -748,7 +748,7 @@ func (s *Service) updateUser(
}
}
if err := webhook.InsertData(ctx, tx, scope, config.OrganizationID, coredata.WebhookEventTypeUserUpdated, webhooktypes.NewUser(profile)); err != nil {
if err := webhook.InsertData(ctx, tx, scope, config.OrganizationID, coredata.WebhookEventTypeUserUpdated, webhooktypes.NewUser(profile, membership)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
@@ -797,25 +797,30 @@ func (s *Service) DeleteUser(
return fmt.Errorf("cannot expire pending invitations: %w", err)
}
membership := &coredata.Membership{}
if err := membership.LoadByIdentityIDAndOrganizationID(
var membership *coredata.Membership
m := &coredata.Membership{}
if err := m.LoadByIdentityIDAndOrganizationID(
ctx, tx, scope, profile.IdentityID, config.OrganizationID,
); err != nil {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load membership: %w", err)
}
} else {
if err := membership.Delete(ctx, tx, scope, membership.ID); err != nil {
return fmt.Errorf("cannot delete membership: %w", err)
}
membership = m
}
if err := webhook.InsertData(ctx, tx, scope, config.OrganizationID, coredata.WebhookEventTypeUserDeleted, webhooktypes.NewUser(profile, membership)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
}
if err := profile.Delete(ctx, tx, scope, profile.ID); err != nil {
return fmt.Errorf("cannot delete profile: %w", err)
}
if err := webhook.InsertData(ctx, tx, scope, config.OrganizationID, coredata.WebhookEventTypeUserDeleted, webhooktypes.NewUser(profile)); err != nil {
return fmt.Errorf("cannot insert webhook event: %w", err)
if membership != nil {
if err := membership.Delete(ctx, tx, scope, membership.ID); err != nil {
return fmt.Errorf("cannot delete membership: %w", err)
}
}
return nil

View File

@@ -612,6 +612,7 @@ func (impl *Implm) Run(
Interval: time.Duration(impl.cfg.Notifications.Webhook.SenderInterval) * time.Second,
CacheTTL: time.Duration(impl.cfg.Notifications.Webhook.CacheTTL) * time.Second,
EncryptionKey: encryptionKey,
Host: baseURL.String(),
})
wg.Go(
func() {

View File

@@ -43,6 +43,7 @@ type (
logger *log.Logger
httpClient *http.Client
encryptionKey cipher.EncryptionKey
host string
cache sync.Map
cacheCreatedAt time.Time
cacheTTL time.Duration
@@ -60,6 +61,7 @@ type (
Timeout time.Duration
CacheTTL time.Duration
EncryptionKey cipher.EncryptionKey
Host string
}
pendingDelivery struct {
@@ -88,6 +90,7 @@ func NewSender(pg *pg.Client, logger *log.Logger, cfg Config) *Sender {
logger: logger,
httpClient: httpclient.DefaultPooledClient(httpclient.WithLogger(logger)),
encryptionKey: cfg.EncryptionKey,
host: cfg.Host,
cacheCreatedAt: time.Now(),
cacheTTL: cfg.CacheTTL,
interval: cfg.Interval,
@@ -310,6 +313,7 @@ func (s *Sender) doHTTPCall(
req.Header.Set("X-Probo-Webhook-Organization-Id", webhookData.OrganizationID.String())
req.Header.Set("X-Probo-Webhook-Timestamp", timestamp)
req.Header.Set("X-Probo-Webhook-Signature", signature)
req.Header.Set("X-Probo-Webhook-Host", s.host)
resp, err := s.httpClient.Do(req)
if err != nil {

View File

@@ -22,31 +22,38 @@ import (
"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 *string `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"`
}
type (
User struct {
ID gid.GID `json:"id"`
OrganizationID gid.GID `json:"organizationId"`
EmailAddress mail.Addr `json:"emailAddress"`
FullName string `json:"fullName"`
Kind *string `json:"kind"`
Source coredata.ProfileSource `json:"source"`
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"`
Membership *UserMembership `json:"membership"`
}
func NewUser(p *coredata.MembershipProfile) *User {
return &User{
UserMembership struct {
ID gid.GID `json:"id"`
Role coredata.MembershipRole `json:"role"`
State coredata.ProfileState `json:"state"`
}
)
func NewUser(p *coredata.MembershipProfile, m *coredata.Membership) *User {
u := &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,
@@ -54,4 +61,14 @@ func NewUser(p *coredata.MembershipProfile) *User {
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
if m != nil {
u.Membership = &UserMembership{
ID: m.ID,
Role: m.Role,
State: p.State,
}
}
return u
}