diff --git a/internal/api/grpc/admin/sms.go b/internal/api/grpc/admin/sms.go index 18e11b23d..8517a8ff2 100644 --- a/internal/api/grpc/admin/sms.go +++ b/internal/api/grpc/admin/sms.go @@ -34,23 +34,23 @@ func (s *Server) GetSMSProvider(ctx context.Context, req *admin_pb.GetSMSProvide } func (s *Server) AddSMSProviderTwilio(ctx context.Context, req *admin_pb.AddSMSProviderTwilioRequest) (*admin_pb.AddSMSProviderTwilioResponse, error) { - id, result, err := s.command.AddSMSConfigTwilio(ctx, authz.GetInstance(ctx).InstanceID(), AddSMSConfigTwilioToConfig(req)) - if err != nil { + smsConfig := addSMSConfigTwilioToConfig(ctx, req) + if err := s.command.AddSMSConfigTwilio(ctx, smsConfig); err != nil { return nil, err } return &admin_pb.AddSMSProviderTwilioResponse{ - Details: object.DomainToAddDetailsPb(result), - Id: id, + Details: object.DomainToAddDetailsPb(smsConfig.Details), + Id: smsConfig.ID, }, nil } func (s *Server) UpdateSMSProviderTwilio(ctx context.Context, req *admin_pb.UpdateSMSProviderTwilioRequest) (*admin_pb.UpdateSMSProviderTwilioResponse, error) { - result, err := s.command.ChangeSMSConfigTwilio(ctx, authz.GetInstance(ctx).InstanceID(), req.Id, UpdateSMSConfigTwilioToConfig(req)) - if err != nil { + smsConfig := updateSMSConfigTwilioToConfig(ctx, req) + if err := s.command.ChangeSMSConfigTwilio(ctx, smsConfig); err != nil { return nil, err } return &admin_pb.UpdateSMSProviderTwilioResponse{ - Details: object.DomainToChangeDetailsPb(result), + Details: object.DomainToChangeDetailsPb(smsConfig.Details), }, nil } @@ -65,6 +65,27 @@ func (s *Server) UpdateSMSProviderTwilioToken(ctx context.Context, req *admin_pb }, nil } +func (s *Server) AddSMSProviderHTTP(ctx context.Context, req *admin_pb.AddSMSProviderHTTPRequest) (*admin_pb.AddSMSProviderHTTPResponse, error) { + smsConfig := addSMSConfigHTTPToConfig(ctx, req) + if err := s.command.AddSMSConfigHTTP(ctx, smsConfig); err != nil { + return nil, err + } + return &admin_pb.AddSMSProviderHTTPResponse{ + Details: object.DomainToAddDetailsPb(smsConfig.Details), + Id: smsConfig.ID, + }, nil +} + +func (s *Server) UpdateSMSProviderHTTP(ctx context.Context, req *admin_pb.UpdateSMSProviderHTTPRequest) (*admin_pb.UpdateSMSProviderHTTPResponse, error) { + smsConfig := updateSMSConfigHTTPToConfig(ctx, req) + if err := s.command.ChangeSMSConfigHTTP(ctx, smsConfig); err != nil { + return nil, err + } + return &admin_pb.UpdateSMSProviderHTTPResponse{ + Details: object.DomainToChangeDetailsPb(smsConfig.Details), + }, nil +} + func (s *Server) ActivateSMSProvider(ctx context.Context, req *admin_pb.ActivateSMSProviderRequest) (*admin_pb.ActivateSMSProviderResponse, error) { result, err := s.command.ActivateSMSConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id) if err != nil { diff --git a/internal/api/grpc/admin/sms_converter.go b/internal/api/grpc/admin/sms_converter.go index 08ca931a7..d13b68f55 100644 --- a/internal/api/grpc/admin/sms_converter.go +++ b/internal/api/grpc/admin/sms_converter.go @@ -1,9 +1,14 @@ package admin import ( + "context" + + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" settings_pb "github.com/zitadel/zitadel/pkg/grpc/settings" @@ -30,10 +35,11 @@ func SMSConfigsToPb(configs []*query.SMSConfig) []*settings_pb.SMSProvider { func SMSConfigToProviderPb(config *query.SMSConfig) *settings_pb.SMSProvider { return &settings_pb.SMSProvider{ - Details: object.ToViewDetailsPb(config.Sequence, config.CreationDate, config.ChangeDate, config.ResourceOwner), - Id: config.ID, - State: smsStateToPb(config.State), - Config: SMSConfigToPb(config), + Details: object.ToViewDetailsPb(config.Sequence, config.CreationDate, config.ChangeDate, config.ResourceOwner), + Id: config.ID, + Description: config.Description, + State: smsStateToPb(config.State), + Config: SMSConfigToPb(config), } } @@ -41,9 +47,20 @@ func SMSConfigToPb(config *query.SMSConfig) settings_pb.SMSConfig { if config.TwilioConfig != nil { return TwilioConfigToPb(config.TwilioConfig) } + if config.HTTPConfig != nil { + return HTTPConfigToPb(config.HTTPConfig) + } return nil } +func HTTPConfigToPb(http *query.HTTP) *settings_pb.SMSProvider_Http { + return &settings_pb.SMSProvider_Http{ + Http: &settings_pb.HTTPConfig{ + Endpoint: http.Endpoint, + }, + } +} + func TwilioConfigToPb(twilio *query.Twilio) *settings_pb.SMSProvider_Twilio { return &settings_pb.SMSProvider_Twilio{ Twilio: &settings_pb.TwilioConfig{ @@ -64,17 +81,39 @@ func smsStateToPb(state domain.SMSConfigState) settings_pb.SMSProviderConfigStat } } -func AddSMSConfigTwilioToConfig(req *admin_pb.AddSMSProviderTwilioRequest) *twilio.Config { - return &twilio.Config{ - SID: req.Sid, - SenderNumber: req.SenderNumber, - Token: req.Token, +func addSMSConfigTwilioToConfig(ctx context.Context, req *admin_pb.AddSMSProviderTwilioRequest) *command.AddTwilioConfig { + return &command.AddTwilioConfig{ + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + Description: req.Description, + SID: req.Sid, + SenderNumber: req.SenderNumber, + Token: req.Token, } } -func UpdateSMSConfigTwilioToConfig(req *admin_pb.UpdateSMSProviderTwilioRequest) *twilio.Config { - return &twilio.Config{ - SID: req.Sid, - SenderNumber: req.SenderNumber, +func updateSMSConfigTwilioToConfig(ctx context.Context, req *admin_pb.UpdateSMSProviderTwilioRequest) *command.ChangeTwilioConfig { + return &command.ChangeTwilioConfig{ + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + ID: req.Id, + Description: gu.Ptr(req.Description), + SID: gu.Ptr(req.Sid), + SenderNumber: gu.Ptr(req.SenderNumber), + } +} + +func addSMSConfigHTTPToConfig(ctx context.Context, req *admin_pb.AddSMSProviderHTTPRequest) *command.AddSMSHTTP { + return &command.AddSMSHTTP{ + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + Description: req.GetDescription(), + Endpoint: req.GetEndpoint(), + } +} + +func updateSMSConfigHTTPToConfig(ctx context.Context, req *admin_pb.UpdateSMSProviderHTTPRequest) *command.ChangeSMSHTTP { + return &command.ChangeSMSHTTP{ + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + ID: req.Id, + Description: gu.Ptr(req.Description), + Endpoint: gu.Ptr(req.Endpoint), } } diff --git a/internal/command/sms_config.go b/internal/command/sms_config.go index cd5e38e51..82eae763d 100644 --- a/internal/command/sms_config.go +++ b/internal/command/sms_config.go @@ -5,203 +5,328 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddSMSConfigTwilio(ctx context.Context, instanceID string, config *twilio.Config) (string, *domain.ObjectDetails, error) { - id, err := c.idGenerator.Next() - if err != nil { - return "", nil, err +type AddTwilioConfig struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description string + SID string + Token string + SenderNumber string +} + +func (c *Commands) AddSMSConfigTwilio(ctx context.Context, config *AddTwilioConfig) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-ZLrZhKSKq0", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if config.ID == "" { + config.ID, err = c.idGenerator.Next() + if err != nil { + return err + } + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID) if err != nil { - return "", nil, err + return err } var token *crypto.CryptoValue if config.Token != "" { token, err = crypto.Encrypt([]byte(config.Token), c.smsEncryption) if err != nil { - return "", nil, err + return err } } - - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTwilioAddedEvent( - ctx, - iamAgg, - id, - config.SID, - config.SenderNumber, - token)) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigTwilioAddedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + config.ID, + config.Description, + config.SID, + config.SenderNumber, + token, + ), + ) if err != nil { - return "", nil, err + return err } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) - if err != nil { - return "", nil, err - } - return id, writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil } -func (c *Commands) ChangeSMSConfigTwilio(ctx context.Context, instanceID, id string, config *twilio.Config) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMS-e9jwf", "Errors.IDMissing") +type ChangeTwilioConfig struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description *string + SID *string + Token *string + SenderNumber *string +} + +func (c *Commands) ChangeSMSConfigTwilio(ctx context.Context, config *ChangeTwilioConfig) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-RHXryJwmFG", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if config.ID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-gMr93iNhTR", "Errors.IDMissing") + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID) if err != nil { - return nil, err + return err } if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.Twilio == nil { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-2m9fw", "Errors.SMSConfig.NotFound") + return zerrors.ThrowNotFound(nil, "COMMAND-MUY0IFAf8O", "Errors.SMSConfig.NotFound") } - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - - changedEvent, hasChanged, err := smsConfigWriteModel.NewChangedEvent( + changedEvent, hasChanged, err := smsConfigWriteModel.NewTwilioChangedEvent( ctx, - iamAgg, - id, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + config.ID, + config.Description, config.SID, config.SenderNumber) if err != nil { - return nil, err + return err } if !hasChanged { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-jf9wk", "Errors.NoChangesFound") + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil } - pushedEvents, err := c.eventstore.Push(ctx, changedEvent) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + changedEvent, + ) if err != nil { - return nil, err + return err } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil } -func (c *Commands) ChangeSMSConfigTwilioToken(ctx context.Context, instanceID, id, token string) (*domain.ObjectDetails, error) { - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) +func (c *Commands) ChangeSMSConfigTwilioToken(ctx context.Context, resourceOwner, id, token string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-sLLA1HnMzj", "Errors.ResourceOwnerMissing") + } + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "SMS-PeNaqbC0r0", "Errors.IDMissing") + } + + smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id) if err != nil { return nil, err } if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.Twilio == nil { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-fj9wf", "Errors.SMSConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-ij3NhEHATp", "Errors.SMSConfig.NotFound") } - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) newtoken, err := crypto.Encrypt([]byte(token), c.smsEncryption) if err != nil { return nil, err } - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTokenChangedEvent( - ctx, - iamAgg, - id, - newtoken)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigTokenChangedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + id, + newtoken, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil } -func (c *Commands) ActivateSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMS-dn93n", "Errors.IDMissing") +type AddSMSHTTP struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description string + Endpoint string +} + +func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-huy99qWjX4", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if config.ID == "" { + config.ID, err = c.idGenerator.Next() + if err != nil { + return err + } + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID) + if err != nil { + return err + } + + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigHTTPAddedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + config.ID, + config.Description, + config.Endpoint, + ), + ) + if err != nil { + return err + } + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil +} + +type ChangeSMSHTTP struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description *string + Endpoint *string +} + +func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTTP) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-M622CFQnwK", "Errors.ResourceOwnerMissing") + } + if config.ID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-phyb2e4Kll", "Errors.IDMissing") + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID) + if err != nil { + return err + } + if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.HTTP == nil { + return zerrors.ThrowNotFound(nil, "COMMAND-6NW4I5Kqzj", "Errors.SMSConfig.NotFound") + } + changedEvent, hasChanged, err := smsConfigWriteModel.NewHTTPChangedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + config.ID, + config.Description, + config.Endpoint) + if err != nil { + return err + } + if !hasChanged { + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, smsConfigWriteModel, changedEvent) + if err != nil { + return err + } + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil +} + +func (c *Commands) ActivateSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-EFgoOg997V", "Errors.ResourceOwnerMissing") + } + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-jJ6TVqzvjp", "Errors.IDMissing") + } + + smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id) if err != nil { return nil, err } if !smsConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-9ULtp9PH5E", "Errors.SMSConfig.NotFound") } if smsConfigWriteModel.State == domain.SMSConfigStateActive { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.AlreadyActive") + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-B25GFeIvRi", "Errors.SMSConfig.AlreadyActive") } - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTwilioActivatedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, smsConfigWriteModel, + instance.NewSMSConfigActivatedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil } -func (c *Commands) DeactivateSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMS-frkwf", "Errors.IDMissing") +func (c *Commands) DeactivateSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-V9NWOZj8Gi", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xs1ah1v1CL", "Errors.IDMissing") + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id) if err != nil { return nil, err } if !smsConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-s39Kg", "Errors.SMSConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-La91dGNhbM", "Errors.SMSConfig.NotFound") } if smsConfigWriteModel.State == domain.SMSConfigStateInactive { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-dm9e3", "Errors.SMSConfig.AlreadyDeactivated") + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-OSZAEkYvk7", "Errors.SMSConfig.AlreadyDeactivated") } - - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigDeactivatedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigDeactivatedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil } -func (c *Commands) RemoveSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMS-3j9fs", "Errors.IDMissing") +func (c *Commands) RemoveSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-cw0NSJsn1v", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qrz7lvdC4c", "Errors.IDMissing") + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id) if err != nil { return nil, err } if !smsConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-povEVHPCkV", "Errors.SMSConfig.NotFound") } - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigRemovedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigRemovedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil } + func (c *Commands) getSMSConfig(ctx context.Context, instanceID, id string) (_ *IAMSMSConfigWriteModel, err error) { writeModel := NewIAMSMSConfigWriteModel(instanceID, id) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - return writeModel, nil } diff --git a/internal/command/sms_config_model.go b/internal/command/sms_config_model.go index 2c215ebf6..8c0e7585d 100644 --- a/internal/command/sms_config_model.go +++ b/internal/command/sms_config_model.go @@ -12,9 +12,11 @@ import ( type IAMSMSConfigWriteModel struct { eventstore.WriteModel - ID string - Twilio *TwilioConfig - State domain.SMSConfigState + ID string + Description string + Twilio *TwilioConfig + HTTP *HTTPConfig + State domain.SMSConfigState } type TwilioConfig struct { @@ -23,6 +25,10 @@ type TwilioConfig struct { SenderNumber string } +type HTTPConfig struct { + Endpoint string +} + func NewIAMSMSConfigWriteModel(instanceID, id string) *IAMSMSConfigWriteModel { return &IAMSMSConfigWriteModel{ WriteModel: eventstore.WriteModel{ @@ -46,11 +52,15 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { Token: e.Token, SenderNumber: e.SenderNumber, } + wm.Description = e.Description wm.State = domain.SMSConfigStateInactive case *instance.SMSConfigTwilioChangedEvent: if wm.ID != e.ID { continue } + if e.Description != nil { + wm.Description = *e.Description + } if e.SID != nil { wm.Twilio.SID = *e.SID } @@ -62,6 +72,42 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { continue } wm.Twilio.Token = e.Token + case *instance.SMSConfigHTTPAddedEvent: + if wm.ID != e.ID { + continue + } + wm.HTTP = &HTTPConfig{ + Endpoint: e.Endpoint, + } + wm.Description = e.Description + wm.State = domain.SMSConfigStateInactive + case *instance.SMSConfigHTTPChangedEvent: + if wm.ID != e.ID { + continue + } + if e.Description != nil { + wm.Description = *e.Description + } + if e.Endpoint != nil { + wm.HTTP.Endpoint = *e.Endpoint + } + case *instance.SMSConfigTwilioActivatedEvent: + if wm.ID != e.ID { + continue + } + wm.State = domain.SMSConfigStateActive + case *instance.SMSConfigTwilioDeactivatedEvent: + if wm.ID != e.ID { + continue + } + wm.State = domain.SMSConfigStateInactive + case *instance.SMSConfigTwilioRemovedEvent: + if wm.ID != e.ID { + continue + } + wm.Twilio = nil + wm.HTTP = nil + wm.State = domain.SMSConfigStateRemoved case *instance.SMSConfigActivatedEvent: if wm.ID != e.ID { continue @@ -77,6 +123,7 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { continue } wm.Twilio = nil + wm.HTTP = nil wm.State = domain.SMSConfigStateRemoved } } @@ -92,21 +139,33 @@ func (wm *IAMSMSConfigWriteModel) Query() *eventstore.SearchQueryBuilder { instance.SMSConfigTwilioAddedEventType, instance.SMSConfigTwilioChangedEventType, instance.SMSConfigTwilioTokenChangedEventType, + instance.SMSConfigHTTPAddedEventType, + instance.SMSConfigHTTPChangedEventType, + instance.SMSConfigTwilioActivatedEventType, + instance.SMSConfigTwilioDeactivatedEventType, + instance.SMSConfigTwilioRemovedEventType, instance.SMSConfigActivatedEventType, instance.SMSConfigDeactivatedEventType, instance.SMSConfigRemovedEventType). Builder() } -func (wm *IAMSMSConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id, sid, senderNumber string) (*instance.SMSConfigTwilioChangedEvent, bool, error) { +func (wm *IAMSMSConfigWriteModel) NewTwilioChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, sid, senderNumber *string) (*instance.SMSConfigTwilioChangedEvent, bool, error) { changes := make([]instance.SMSConfigTwilioChanges, 0) var err error - if wm.Twilio.SID != sid { - changes = append(changes, instance.ChangeSMSConfigTwilioSID(sid)) + if wm.Twilio == nil { + return nil, false, nil } - if wm.Twilio.SenderNumber != senderNumber { - changes = append(changes, instance.ChangeSMSConfigTwilioSenderNumber(senderNumber)) + + if description != nil && wm.Description != *description { + changes = append(changes, instance.ChangeSMSConfigTwilioDescription(*description)) + } + if sid != nil && wm.Twilio.SID != *sid { + changes = append(changes, instance.ChangeSMSConfigTwilioSID(*sid)) + } + if senderNumber != nil && wm.Twilio.SenderNumber != *senderNumber { + changes = append(changes, instance.ChangeSMSConfigTwilioSenderNumber(*senderNumber)) } if len(changes) == 0 { @@ -118,3 +177,28 @@ func (wm *IAMSMSConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate } return changeEvent, true, nil } + +func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, endpoint *string) (*instance.SMSConfigHTTPChangedEvent, bool, error) { + changes := make([]instance.SMSConfigHTTPChanges, 0) + var err error + + if wm.HTTP == nil { + return nil, false, nil + } + + if description != nil && wm.Description != *description { + changes = append(changes, instance.ChangeSMSConfigHTTPDescription(*description)) + } + if endpoint != nil && wm.HTTP.Endpoint != *endpoint { + changes = append(changes, instance.ChangeSMSConfigHTTPEndpoint(*endpoint)) + } + + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := instance.NewSMSConfigHTTPChangedEvent(ctx, aggregate, id, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil +} diff --git a/internal/command/sms_config_test.go b/internal/command/sms_config_test.go index 8d9675194..b0936ab8f 100644 --- a/internal/command/sms_config_test.go +++ b/internal/command/sms_config_test.go @@ -2,8 +2,10 @@ package command import ( "context" + "errors" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -12,21 +14,19 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" id_mock "github.com/zitadel/zitadel/internal/id/mock" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore idGenerator id.Generator alg crypto.EncryptionAlgorithm } type args struct { - ctx context.Context - instanceID string - sms *twilio.Config + ctx context.Context + sms *AddTwilioConfig } type res struct { want *domain.ObjectDetails @@ -38,17 +38,32 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { args args res res }{ + { + name: "add sms config twilio, missing resourceowner", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + sms: &AddTwilioConfig{}, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-ZLrZhKSKq0", "Errors.ResourceOwnerMissing")) + }, + }, + }, { name: "add sms config twilio, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectPush( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "senderName", &crypto.CryptoValue{ @@ -64,12 +79,13 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: context.Background(), - instanceID: "INSTANCE", - sms: &twilio.Config{ - SID: "sid", - Token: "token", - SenderNumber: "senderName", + ctx: context.Background(), + sms: &AddTwilioConfig{ + ResourceOwner: "INSTANCE", + Description: "description", + SID: "sid", + Token: "token", + SenderNumber: "senderName", }, }, res: res{ @@ -82,11 +98,11 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, smsEncryption: tt.fields.alg, } - _, got, err := r.AddSMSConfigTwilio(tt.args.ctx, tt.args.instanceID, tt.args.sms) + err := r.AddSMSConfigTwilio(tt.args.ctx, tt.args.sms) if tt.res.err == nil { assert.NoError(t, err) } @@ -94,7 +110,7 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assertObjectDetails(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, tt.args.sms.Details) } }) } @@ -102,13 +118,11 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - instanceID string - id string - sms *twilio.Config + ctx context.Context + sms *ChangeTwilioConfig } type res struct { want *domain.ObjectDetails @@ -120,50 +134,70 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { args args res res }{ + { - name: "id empty, precondition error", + name: "resourceowner empty, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), - sms: &twilio.Config{}, + sms: &ChangeTwilioConfig{}, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-RHXryJwmFG", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + sms: &ChangeTwilioConfig{ + ResourceOwner: "INSTANCE", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-gMr93iNhTR", "Errors.IDMissing")) + }, }, }, { name: "sms not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, args: args{ - ctx: context.Background(), - sms: &twilio.Config{}, - instanceID: "INSTANCE", - id: "id", + ctx: context.Background(), + sms: &ChangeTwilioConfig{ + ResourceOwner: "INSTANCE", + ID: "id", + }, }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-MUY0IFAf8O", "Errors.SMSConfig.NotFound")) + }, }, }, { - name: "no changes, precondition error", + name: "no changes", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "senderName", &crypto.CryptoValue{ @@ -179,29 +213,31 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { }, args: args{ ctx: context.Background(), - sms: &twilio.Config{ - SID: "sid", - Token: "token", - SenderNumber: "senderName", + sms: &ChangeTwilioConfig{ + ResourceOwner: "INSTANCE", + ID: "providerid", + SID: gu.Ptr("sid"), + Token: gu.Ptr("token"), + SenderNumber: gu.Ptr("senderName"), }, - instanceID: "INSTANCE", - id: "providerid", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, }, }, { name: "sms config twilio change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "token", &crypto.CryptoValue{ @@ -219,19 +255,21 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { "providerid", "sid2", "senderName2", + "description2", ), ), ), }, args: args{ ctx: context.Background(), - sms: &twilio.Config{ - SID: "sid2", - Token: "token2", - SenderNumber: "senderName2", + sms: &ChangeTwilioConfig{ + ResourceOwner: "INSTANCE", + ID: "providerid", + Description: gu.Ptr("description2"), + SID: gu.Ptr("sid2"), + Token: gu.Ptr("token2"), + SenderNumber: gu.Ptr("senderName2"), }, - instanceID: "INSTANCE", - id: "providerid", }, res: res{ want: &domain.ObjectDetails{ @@ -243,9 +281,9 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.ChangeSMSConfigTwilio(tt.args.ctx, tt.args.instanceID, tt.args.id, tt.args.sms) + err := r.ChangeSMSConfigTwilio(tt.args.ctx, tt.args.sms) if tt.res.err == nil { assert.NoError(t, err) } @@ -253,15 +291,264 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assertObjectDetails(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, tt.args.sms.Details) } }) } } -func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { +func TestCommandSide_AddSMSConfigHTTP(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + alg crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + http *AddSMSHTTP + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "add sms config http, resource owner missing", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + http: &AddSMSHTTP{}, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-huy99qWjX4", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "add sms config http, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"), + }, + args: args{ + ctx: context.Background(), + http: &AddSMSHTTP{ + ResourceOwner: "INSTANCE", + Description: "description", + Endpoint: "endpoint", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + smsEncryption: tt.fields.alg, + } + err := r.AddSMSConfigHTTP(tt.args.ctx, tt.args.http) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.http.Details) + } + }) + } +} + +func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + ctx context.Context + http *ChangeSMSHTTP + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{}, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-M622CFQnwK", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{ + ResourceOwner: "INSTANCE", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-phyb2e4Kll", "Errors.IDMissing")) + }, + }, + }, + { + name: "sms not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{ + ResourceOwner: "INSTANCE", + ID: "id", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-6NW4I5Kqzj", "Errors.SMSConfig.NotFound")) + }, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{ + ResourceOwner: "INSTANCE", + ID: "providerid", + Endpoint: gu.Ptr("endpoint"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "sms config http change, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + expectPush( + newSMSConfigHTTPChangedEvent( + context.Background(), + "providerid", + "endpoint2", + "description2", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{ + ResourceOwner: "INSTANCE", + ID: "providerid", + Description: gu.Ptr("description2"), + Endpoint: gu.Ptr("endpoint2"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + } + err := r.ChangeSMSConfigHTTP(tt.args.ctx, tt.args.http) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.http.Details) + } + }) + } +} + +func TestCommandSide_ActivateSMSConfig(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -279,24 +566,38 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { res res }{ { - name: "id empty, invalid error", + name: "resourceowner empty, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-EFgoOg997V", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, invalid error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-jJ6TVqzvjp", "Errors.IDMissing")) + }, }, }, { name: "sms not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -306,20 +607,59 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { id: "id", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-9ULtp9PH5E", "Errors.SMSConfig.NotFound")) + }, }, }, { - name: "sms config twilio activate, ok", + name: "sms existing, already active", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", + "sid", + "sender-name", + &crypto.CryptoValue{}, + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-B25GFeIvRi", "Errors.SMSConfig.AlreadyActive")) + }, + }, + }, + { + name: "sms config twilio activate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", "sid", "sender-name", &crypto.CryptoValue{}, @@ -327,7 +667,42 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { ), ), expectPush( - instance.NewSMSConfigTwilioActivatedEvent( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "sms config http activate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + expectPush( + instance.NewSMSConfigActivatedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", @@ -350,7 +725,7 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.ActivateSMSConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) if tt.res.err == nil { @@ -366,9 +741,9 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { } } -func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { +func TestCommandSide_DeactivateSMSConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -384,26 +759,39 @@ func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { fields fields args args res res - }{ + }{{ + name: "resourceowner empty, invalid error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-V9NWOZj8Gi", "Errors.ResourceOwnerMissing")) + }, + }, + }, { name: "id empty, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: context.Background(), + instanceID: "INSTANCE", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-xs1ah1v1CL", "Errors.IDMissing")) + }, }, }, { name: "sms not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -413,27 +801,115 @@ func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { id: "id", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-La91dGNhbM", "Errors.SMSConfig.NotFound")) + }, }, }, { - name: "sms config twilio deactivate, ok", + name: "sms config twilio deactivate, already deactivated", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "sender-name", &crypto.CryptoValue{}, ), ), eventFromEventPusher( - instance.NewSMSConfigTwilioActivatedEvent( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigDeactivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-OSZAEkYvk7", "Errors.SMSConfig.AlreadyDeactivated")) + }, + }, + }, + { + name: "sms config twilio deactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "sid", + "sender-name", + &crypto.CryptoValue{}, + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + expectPush( + instance.NewSMSConfigDeactivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "sms config http deactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", @@ -464,7 +940,7 @@ func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.DeactivateSMSConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) if tt.res.err == nil { @@ -482,7 +958,7 @@ func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { func TestCommandSide_RemoveSMSConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -500,24 +976,38 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { res res }{ { - name: "id empty, invalid error", + name: "resourceowner empty, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-cw0NSJsn1v", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, invalid error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qrz7lvdC4c", "Errors.IDMissing")) + }, }, }, { name: "sms not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -527,20 +1017,22 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { id: "id", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-povEVHPCkV", "Errors.SMSConfig.NotFound")) + }, }, }, { - name: "sms config remove, ok", + name: "sms config remove, twilio, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "sender-name", &crypto.CryptoValue{}, @@ -567,11 +1059,46 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { }, }, }, + { + name: "sms config remove, http, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + expectPush( + instance.NewSMSConfigRemovedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.RemoveSMSConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) if tt.res.err == nil { @@ -587,10 +1114,11 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { } } -func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName string) *instance.SMSConfigTwilioChangedEvent { +func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName, description string) *instance.SMSConfigTwilioChangedEvent { changes := []instance.SMSConfigTwilioChanges{ instance.ChangeSMSConfigTwilioSID(sid), instance.ChangeSMSConfigTwilioSenderNumber(senderName), + instance.ChangeSMSConfigTwilioDescription(description), } event, _ := instance.NewSMSConfigTwilioChangedEvent(ctx, &instance.NewAggregate("INSTANCE").Aggregate, @@ -599,3 +1127,16 @@ func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName str ) return event } + +func newSMSConfigHTTPChangedEvent(ctx context.Context, id, endpoint, description string) *instance.SMSConfigHTTPChangedEvent { + changes := []instance.SMSConfigHTTPChanges{ + instance.ChangeSMSConfigHTTPEndpoint(endpoint), + instance.ChangeSMSConfigHTTPDescription(description), + } + event, _ := instance.NewSMSConfigHTTPChangedEvent(ctx, + &instance.NewAggregate("INSTANCE").Aggregate, + id, + changes, + ) + return event +} diff --git a/internal/command/smtp_test.go b/internal/command/smtp_test.go index b6bb7d98a..17f7f088d 100644 --- a/internal/command/smtp_test.go +++ b/internal/command/smtp_test.go @@ -953,6 +953,49 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { }, }, }, + { + name: "activate smtp config, already active, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + true, + "from", + "name", + "", + "host:587", + "user", + &crypto.CryptoValue{}, + ), + ), + ), + expectPush( + instance.NewSMTPConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + id: "ID", + instanceID: "INSTANCE", + activatedId: "", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/notification/channels.go b/internal/notification/channels.go index 68ad67347..0511aaf78 100644 --- a/internal/notification/channels.go +++ b/internal/notification/channels.go @@ -5,8 +5,8 @@ import ( "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/notification/channels/webhook" "github.com/zitadel/zitadel/internal/notification/handlers" "github.com/zitadel/zitadel/internal/notification/senders" @@ -78,20 +78,20 @@ func (c *channels) Email(ctx context.Context) (*senders.Chain, *smtp.Config, err return chain, smtpCfg, err } -func (c *channels) SMS(ctx context.Context) (*senders.Chain, *twilio.Config, error) { - twilioCfg, err := c.q.GetTwilioConfig(ctx) +func (c *channels) SMS(ctx context.Context) (*senders.Chain, *sms.Config, error) { + smsCfg, err := c.q.GetActiveSMSConfig(ctx) if err != nil { return nil, nil, err } chain, err := senders.SMSChannels( ctx, - twilioCfg, + smsCfg, c.q.GetFileSystemProvider, c.q.GetLogProvider, c.counters.success.sms, c.counters.failed.sms, ) - return chain, twilioCfg, err + return chain, smsCfg, err } func (c *channels) Webhook(ctx context.Context, cfg webhook.Config) (*senders.Chain, error) { diff --git a/internal/notification/channels/sms/config.go b/internal/notification/channels/sms/config.go new file mode 100644 index 000000000..d75927208 --- /dev/null +++ b/internal/notification/channels/sms/config.go @@ -0,0 +1,17 @@ +package sms + +import ( + "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" +) + +type Config struct { + ProviderConfig *Provider + TwilioConfig *twilio.Config + WebhookConfig *webhook.Config +} + +type Provider struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` +} diff --git a/internal/notification/handlers/config_sms.go b/internal/notification/handlers/config_sms.go new file mode 100644 index 000000000..4698772ea --- /dev/null +++ b/internal/notification/handlers/config_sms.go @@ -0,0 +1,52 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/notification/channels/sms" + "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" + "github.com/zitadel/zitadel/internal/zerrors" +) + +// GetActiveSMSConfig reads the active iam sms provider config +func (n *NotificationQueries) GetActiveSMSConfig(ctx context.Context) (*sms.Config, error) { + config, err := n.SMSProviderConfigActive(ctx, authz.GetInstance(ctx).InstanceID()) + if err != nil { + return nil, err + } + + provider := &sms.Provider{ + ID: config.ID, + Description: config.Description, + } + if config.TwilioConfig != nil { + token, err := crypto.DecryptString(config.TwilioConfig.Token, n.SMSTokenCrypto) + if err != nil { + return nil, err + } + return &sms.Config{ + ProviderConfig: provider, + TwilioConfig: &twilio.Config{ + SID: config.TwilioConfig.SID, + Token: token, + SenderNumber: config.TwilioConfig.SenderNumber, + }, + }, nil + } + if config.HTTPConfig != nil { + return &sms.Config{ + ProviderConfig: provider, + WebhookConfig: &webhook.Config{ + CallURL: config.HTTPConfig.Endpoint, + Method: http.MethodPost, + Headers: nil, + }, + }, nil + } + + return nil, zerrors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound") +} diff --git a/internal/notification/handlers/config_twilio.go b/internal/notification/handlers/config_twilio.go deleted file mode 100644 index 539155369..000000000 --- a/internal/notification/handlers/config_twilio.go +++ /dev/null @@ -1,35 +0,0 @@ -package handlers - -import ( - "context" - - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" -) - -// GetTwilioConfig reads the iam Twilio provider config -func (n *NotificationQueries) GetTwilioConfig(ctx context.Context) (*twilio.Config, error) { - active, err := query.NewSMSProviderStateQuery(domain.SMSConfigStateActive) - if err != nil { - return nil, err - } - config, err := n.SMSProviderConfig(ctx, active) - if err != nil { - return nil, err - } - if config.TwilioConfig == nil { - return nil, zerrors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound") - } - token, err := crypto.DecryptString(config.TwilioConfig.Token, n.SMSTokenCrypto) - if err != nil { - return nil, err - } - return &twilio.Config{ - SID: config.TwilioConfig.SID, - Token: token, - SenderNumber: config.TwilioConfig.SenderNumber, - }, nil -} diff --git a/internal/notification/handlers/mock/queries.mock.go b/internal/notification/handlers/mock/queries.mock.go index 210493d87..48d7ec21e 100644 --- a/internal/notification/handlers/mock/queries.mock.go +++ b/internal/notification/handlers/mock/queries.mock.go @@ -161,24 +161,19 @@ func (mr *MockQueriesMockRecorder) NotificationProviderByIDAndType(arg0, arg1, a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationProviderByIDAndType", reflect.TypeOf((*MockQueries)(nil).NotificationProviderByIDAndType), arg0, arg1, arg2) } -// SMSProviderConfig mocks base method. -func (m *MockQueries) SMSProviderConfig(arg0 context.Context, arg1 ...query.SearchQuery) (*query.SMSConfig, error) { +// SMSProviderConfigActive mocks base method. +func (m *MockQueries) SMSProviderConfigActive(arg0 context.Context, arg1 string) (*query.SMSConfig, error) { m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "SMSProviderConfig", varargs...) + ret := m.ctrl.Call(m, "SMSProviderConfigActive", arg0, arg1) ret0, _ := ret[0].(*query.SMSConfig) ret1, _ := ret[1].(error) return ret0, ret1 } -// SMSProviderConfig indicates an expected call of SMSProviderConfig. -func (mr *MockQueriesMockRecorder) SMSProviderConfig(arg0 any, arg1 ...any) *gomock.Call { +// SMSProviderConfigActive indicates an expected call of SMSProviderConfigActive. +func (mr *MockQueriesMockRecorder) SMSProviderConfigActive(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfig", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfig), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfigActive", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfigActive), arg0, arg1) } // SMTPConfigActive mocks base method. diff --git a/internal/notification/handlers/queries.go b/internal/notification/handlers/queries.go index ce7597ead..49cffc5e4 100644 --- a/internal/notification/handlers/queries.go +++ b/internal/notification/handlers/queries.go @@ -21,7 +21,7 @@ type Queries interface { NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (*query.NotificationPolicy, error) SearchMilestones(ctx context.Context, instanceIDs []string, queries *query.MilestonesSearchQueries) (*query.Milestones, error) NotificationProviderByIDAndType(ctx context.Context, aggID string, providerType domain.NotificationProviderType) (*query.DebugNotificationProvider, error) - SMSProviderConfig(ctx context.Context, queries ...query.SearchQuery) (*query.SMSConfig, error) + SMSProviderConfigActive(ctx context.Context, resourceOwner string) (config *query.SMSConfig, err error) SMTPConfigActive(ctx context.Context, resourceOwner string) (*query.SMTPConfig, error) GetDefaultLanguage(ctx context.Context) language.Tag GetInstanceRestrictions(ctx context.Context) (restrictions query.Restrictions, err error) diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index 066796ae3..1826e9d79 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -283,7 +283,7 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler } notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e) if e.NotificationType == domain.NotificationTypeSms { - notify = types.SendSMSTwilio(ctx, u.channels, translator, notifyUser, colors, e) + notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e) } err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID) if err != nil { @@ -373,7 +373,7 @@ func (u *userNotifier) reduceOTPSMS( if err != nil { return nil, err } - notify := types.SendSMSTwilio(ctx, u.channels, translator, notifyUser, colors, event) + notify := types.SendSMS(ctx, u.channels, translator, notifyUser, colors, event) err = notify.SendOTPSMSCode(ctx, plainCode, expiry) if err != nil { return nil, err @@ -709,7 +709,7 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St if err != nil { return err } - err = types.SendSMSTwilio(ctx, u.channels, translator, notifyUser, colors, e). + err = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e). SendPhoneVerificationCode(ctx, code) if err != nil { return err diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index 8b2155cc2..60fa1791d 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -16,8 +16,8 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/repository" es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock" channel_mock "github.com/zitadel/zitadel/internal/notification/channels/mock" + "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/notification/channels/webhook" "github.com/zitadel/zitadel/internal/notification/handlers/mock" "github.com/zitadel/zitadel/internal/notification/messages" @@ -1463,7 +1463,7 @@ func (c *channels) Email(context.Context) (*senders.Chain, *smtp.Config, error) return &c.Chain, nil, nil } -func (c *channels) SMS(context.Context) (*senders.Chain, *twilio.Config, error) { +func (c *channels) SMS(context.Context) (*senders.Chain, *sms.Config, error) { return &c.Chain, nil, nil } diff --git a/internal/notification/senders/sms.go b/internal/notification/senders/sms.go index 361fc5650..7ce2adc71 100644 --- a/internal/notification/senders/sms.go +++ b/internal/notification/senders/sms.go @@ -3,36 +3,60 @@ package senders import ( "context" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/notification/channels" "github.com/zitadel/zitadel/internal/notification/channels/fs" "github.com/zitadel/zitadel/internal/notification/channels/instrumenting" "github.com/zitadel/zitadel/internal/notification/channels/log" + "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" ) const twilioSpanName = "twilio.NotificationChannel" func SMSChannels( ctx context.Context, - twilioConfig *twilio.Config, + smsConfig *sms.Config, getFileSystemProvider func(ctx context.Context) (*fs.Config, error), getLogProvider func(ctx context.Context) (*log.Config, error), successMetricName, failureMetricName string, ) (chain *Chain, err error) { channels := make([]channels.NotificationChannel, 0, 3) - if twilioConfig != nil { + if smsConfig.TwilioConfig != nil { channels = append( channels, instrumenting.Wrap( ctx, - twilio.InitChannel(*twilioConfig), + twilio.InitChannel(*smsConfig.TwilioConfig), twilioSpanName, successMetricName, failureMetricName, ), ) } + if smsConfig.WebhookConfig != nil { + webhookChannel, err := webhook.InitChannel(ctx, *smsConfig.WebhookConfig) + logging.WithFields( + "instance", authz.GetInstance(ctx).InstanceID(), + "callurl", smsConfig.WebhookConfig.CallURL, + ).OnError(err).Debug("initializing JSON channel failed") + if err == nil { + channels = append( + channels, + instrumenting.Wrap( + ctx, + webhookChannel, + webhookSpanName, + successMetricName, + failureMetricName, + ), + ) + } + } channels = append(channels, debugChannels(ctx, getFileSystemProvider, getLogProvider)...) return ChainChannels(channels...), nil } diff --git a/internal/notification/templates/templateData.go b/internal/notification/templates/templateData.go index 8ff750da5..2c3885ef7 100644 --- a/internal/notification/templates/templateData.go +++ b/internal/notification/templates/templateData.go @@ -15,23 +15,23 @@ const ( ) type TemplateData struct { - Title string - PreHeader string - Subject string - Greeting string - Text string - URL string - ButtonText string - PrimaryColor string - BackgroundColor string - FontColor string - LogoURL string - FontURL string - FontFaceFamily string - FontFamily string + Title string `json:"title,omitempty"` + PreHeader string `json:"preHeader,omitempty"` + Subject string `json:"subject,omitempty"` + Greeting string `json:"greeting,omitempty"` + Text string `json:"text,omitempty"` + URL string `json:"url,omitempty"` + ButtonText string `json:"buttonText,omitempty"` + PrimaryColor string `json:"primaryColor,omitempty"` + BackgroundColor string `json:"backgroundColor,omitempty"` + FontColor string `json:"fontColor,omitempty"` + LogoURL string `json:"logoUrl,omitempty"` + FontURL string `json:"fontUrl,omitempty"` + FontFaceFamily string `json:"fontFaceFamily,omitempty"` + FontFamily string `json:"fontFamily,omitempty"` - IncludeFooter bool - FooterText string + IncludeFooter bool `json:"includeFooter,omitempty"` + FooterText string `json:"footerText,omitempty"` } func (data *TemplateData) Translate(translator *i18n.Translator, msgType string, args map[string]interface{}, langs ...string) { diff --git a/internal/notification/types/notification.go b/internal/notification/types/notification.go index 9bbfb66c8..4d72d911f 100644 --- a/internal/notification/types/notification.go +++ b/internal/notification/types/notification.go @@ -7,8 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/notification/channels/webhook" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/templates" @@ -24,7 +24,7 @@ type Notify func( type ChannelChains interface { Email(context.Context) (*senders.Chain, *smtp.Config, error) - SMS(context.Context) (*senders.Chain, *twilio.Config, error) + SMS(context.Context) (*senders.Chain, *sms.Config, error) Webhook(context.Context, webhook.Config) (*senders.Chain, error) } @@ -79,7 +79,7 @@ func sanitizeArgsForHTML(args map[string]any) { } } -func SendSMSTwilio( +func SendSMS( ctx context.Context, channels ChannelChains, translator *i18n.Translator, @@ -99,7 +99,8 @@ func SendSMSTwilio( ctx, channels, user, - data.Text, + data, + args, allowUnverifiedNotificationChannel, triggeringEvent, ) diff --git a/internal/notification/types/user_phone.go b/internal/notification/types/user_phone.go index c9adfeac8..8e79f7371 100644 --- a/internal/notification/types/user_phone.go +++ b/internal/notification/types/user_phone.go @@ -2,40 +2,78 @@ package types import ( "context" + "strings" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/messages" + "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" ) +type serializableData struct { + ContextInfo map[string]interface{} `json:"contextInfo,omitempty"` + TemplateData templates.TemplateData `json:"templateData,omitempty"` + Args map[string]interface{} `json:"args,omitempty"` +} + func generateSms( ctx context.Context, channels ChannelChains, user *query.NotifyUser, - content string, + data templates.TemplateData, + args map[string]interface{}, lastPhone bool, triggeringEvent eventstore.Event, ) error { - number := "" - smsChannels, twilioConfig, err := channels.SMS(ctx) + smsChannels, config, err := channels.SMS(ctx) logging.OnError(err).Error("could not create sms channel") if smsChannels == nil || smsChannels.Len() == 0 { return zerrors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent") } - if err == nil { - number = twilioConfig.SenderNumber - } - message := &messages.SMS{ - SenderPhoneNumber: number, - RecipientPhoneNumber: user.VerifiedPhone, - Content: content, - TriggeringEvent: triggeringEvent, - } + recipient := user.VerifiedPhone if lastPhone { - message.RecipientPhoneNumber = user.LastPhone + recipient = user.LastPhone } - return smsChannels.HandleMessage(message) + if config.TwilioConfig != nil { + number := "" + if err == nil { + number = config.TwilioConfig.SenderNumber + } + message := &messages.SMS{ + SenderPhoneNumber: number, + RecipientPhoneNumber: recipient, + Content: data.Text, + TriggeringEvent: triggeringEvent, + } + return smsChannels.HandleMessage(message) + } + if config.WebhookConfig != nil { + caseArgs := make(map[string]interface{}, len(args)) + for k, v := range args { + caseArgs[strings.ToLower(string(k[0]))+k[1:]] = v + } + contextInfo := map[string]interface{}{ + "recipientPhoneNumber": recipient, + "eventType": triggeringEvent.Type(), + "provider": config.ProviderConfig, + } + + message := &messages.JSON{ + Serializable: &serializableData{ + TemplateData: data, + Args: caseArgs, + ContextInfo: contextInfo, + }, + TriggeringEvent: triggeringEvent, + } + webhookChannels, err := channels.Webhook(ctx, *config.WebhookConfig) + if err != nil { + return err + } + return webhookChannels.HandleMessage(message) + } + return zerrors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent") } diff --git a/internal/query/projection/sms.go b/internal/query/projection/sms.go index 13059203f..9b157ff99 100644 --- a/internal/query/projection/sms.go +++ b/internal/query/projection/sms.go @@ -8,12 +8,12 @@ import ( old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/zerrors" ) const ( - SMSConfigProjectionTable = "projections.sms_configs2" + SMSConfigProjectionTable = "projections.sms_configs3" SMSTwilioTable = SMSConfigProjectionTable + "_" + smsTwilioTableSuffix + SMSHTTPTable = SMSConfigProjectionTable + "_" + smsHTTPTableSuffix SMSColumnID = "id" SMSColumnAggregateID = "aggregate_id" @@ -23,13 +23,19 @@ const ( SMSColumnState = "state" SMSColumnResourceOwner = "resource_owner" SMSColumnInstanceID = "instance_id" + SMSColumnDescription = "description" - smsTwilioTableSuffix = "twilio" - SMSTwilioConfigColumnSMSID = "sms_id" - SMSTwilioColumnInstanceID = "instance_id" - SMSTwilioConfigColumnSID = "sid" - SMSTwilioConfigColumnSenderNumber = "sender_number" - SMSTwilioConfigColumnToken = "token" + smsTwilioTableSuffix = "twilio" + SMSTwilioColumnSMSID = "sms_id" + SMSTwilioColumnInstanceID = "instance_id" + SMSTwilioColumnSID = "sid" + SMSTwilioColumnSenderNumber = "sender_number" + SMSTwilioColumnToken = "token" + + smsHTTPTableSuffix = "http" + SMSHTTPColumnSMSID = "sms_id" + SMSHTTPColumnInstanceID = "instance_id" + SMSHTTPColumnEndpoint = "endpoint" ) type smsConfigProjection struct{} @@ -53,20 +59,30 @@ func (*smsConfigProjection) Init() *old_handler.Check { handler.NewColumn(SMSColumnState, handler.ColumnTypeEnum), handler.NewColumn(SMSColumnResourceOwner, handler.ColumnTypeText), handler.NewColumn(SMSColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(SMSColumnDescription, handler.ColumnTypeText), }, handler.NewPrimaryKey(SMSColumnInstanceID, SMSColumnID), ), handler.NewSuffixedTable([]*handler.InitColumn{ - handler.NewColumn(SMSTwilioConfigColumnSMSID, handler.ColumnTypeText), + handler.NewColumn(SMSTwilioColumnSMSID, handler.ColumnTypeText), handler.NewColumn(SMSTwilioColumnInstanceID, handler.ColumnTypeText), - handler.NewColumn(SMSTwilioConfigColumnSID, handler.ColumnTypeText), - handler.NewColumn(SMSTwilioConfigColumnSenderNumber, handler.ColumnTypeText), - handler.NewColumn(SMSTwilioConfigColumnToken, handler.ColumnTypeJSONB), + handler.NewColumn(SMSTwilioColumnSID, handler.ColumnTypeText), + handler.NewColumn(SMSTwilioColumnSenderNumber, handler.ColumnTypeText), + handler.NewColumn(SMSTwilioColumnToken, handler.ColumnTypeJSONB), }, - handler.NewPrimaryKey(SMSTwilioColumnInstanceID, SMSTwilioConfigColumnSMSID), + handler.NewPrimaryKey(SMSTwilioColumnInstanceID, SMSTwilioColumnSMSID), smsTwilioTableSuffix, handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), ), + handler.NewSuffixedTable([]*handler.InitColumn{ + handler.NewColumn(SMSHTTPColumnSMSID, handler.ColumnTypeText), + handler.NewColumn(SMSHTTPColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(SMSHTTPColumnEndpoint, handler.ColumnTypeText), + }, + handler.NewPrimaryKey(SMSHTTPColumnInstanceID, SMSHTTPColumnSMSID), + smsHTTPTableSuffix, + handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), + ), ) } @@ -87,6 +103,26 @@ func (p *smsConfigProjection) Reducers() []handler.AggregateReducer { Event: instance.SMSConfigTwilioTokenChangedEventType, Reduce: p.reduceSMSConfigTwilioTokenChanged, }, + { + Event: instance.SMSConfigHTTPAddedEventType, + Reduce: p.reduceSMSConfigHTTPAdded, + }, + { + Event: instance.SMSConfigHTTPChangedEventType, + Reduce: p.reduceSMSConfigHTTPChanged, + }, + { + Event: instance.SMSConfigTwilioActivatedEventType, + Reduce: p.reduceSMSConfigTwilioActivated, + }, + { + Event: instance.SMSConfigTwilioDeactivatedEventType, + Reduce: p.reduceSMSConfigTwilioDeactivated, + }, + { + Event: instance.SMSConfigTwilioRemovedEventType, + Reduce: p.reduceSMSConfigTwilioRemoved, + }, { Event: instance.SMSConfigActivatedEventType, Reduce: p.reduceSMSConfigActivated, @@ -109,9 +145,9 @@ func (p *smsConfigProjection) Reducers() []handler.AggregateReducer { } func (p *smsConfigProjection) reduceSMSConfigTwilioAdded(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigTwilioAddedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-s8efs", "reduce.wrong.event.type %s", instance.SMSConfigTwilioAddedEventType) + e, err := assertEvent[*instance.SMSConfigTwilioAddedEvent](event) + if err != nil { + return nil, err } return handler.NewMultiStatement( @@ -126,15 +162,16 @@ func (p *smsConfigProjection) reduceSMSConfigTwilioAdded(event eventstore.Event) handler.NewCol(SMSColumnInstanceID, e.Aggregate().InstanceID), handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), handler.NewCol(SMSColumnSequence, e.Sequence()), + handler.NewCol(SMSColumnDescription, e.Description), }, ), handler.AddCreateStatement( []handler.Column{ - handler.NewCol(SMSTwilioConfigColumnSMSID, e.ID), + handler.NewCol(SMSTwilioColumnSMSID, e.ID), handler.NewCol(SMSTwilioColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCol(SMSTwilioConfigColumnSID, e.SID), - handler.NewCol(SMSTwilioConfigColumnToken, e.Token), - handler.NewCol(SMSTwilioConfigColumnSenderNumber, e.SenderNumber), + handler.NewCol(SMSTwilioColumnSID, e.SID), + handler.NewCol(SMSTwilioColumnToken, e.Token), + handler.NewCol(SMSTwilioColumnSenderNumber, e.SenderNumber), }, handler.WithTableSuffix(smsTwilioTableSuffix), ), @@ -142,57 +179,64 @@ func (p *smsConfigProjection) reduceSMSConfigTwilioAdded(event eventstore.Event) } func (p *smsConfigProjection) reduceSMSConfigTwilioChanged(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigTwilioChangedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-fi99F", "reduce.wrong.event.type %s", instance.SMSConfigTwilioChangedEventType) - } - columns := make([]handler.Column, 0) - if e.SID != nil { - columns = append(columns, handler.NewCol(SMSTwilioConfigColumnSID, *e.SID)) - } - if e.SenderNumber != nil { - columns = append(columns, handler.NewCol(SMSTwilioConfigColumnSenderNumber, *e.SenderNumber)) + e, err := assertEvent[*instance.SMSConfigTwilioChangedEvent](event) + if err != nil { + return nil, err } - return handler.NewMultiStatement( - e, - handler.AddUpdateStatement( + stmts := make([]func(eventstore.Event) handler.Exec, 0, 3) + columns := []handler.Column{ + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + } + if e.Description != nil { + columns = append(columns, handler.NewCol(SMSColumnDescription, *e.Description)) + } + if len(columns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( columns, - []handler.Condition{ - handler.NewCond(SMSTwilioConfigColumnSMSID, e.ID), - handler.NewCond(SMSTwilioColumnInstanceID, e.Aggregate().InstanceID), - }, - handler.WithTableSuffix(smsTwilioTableSuffix), - ), - handler.AddUpdateStatement( - []handler.Column{ - handler.NewCol(SMSColumnChangeDate, e.CreationDate()), - handler.NewCol(SMSColumnSequence, e.Sequence()), - }, []handler.Condition{ handler.NewCond(SMSColumnID, e.ID), handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), }, - ), - ), nil + )) + } + + twilioColumns := make([]handler.Column, 0) + if e.SID != nil { + twilioColumns = append(twilioColumns, handler.NewCol(SMSTwilioColumnSID, *e.SID)) + } + if e.SenderNumber != nil { + twilioColumns = append(twilioColumns, handler.NewCol(SMSTwilioColumnSenderNumber, *e.SenderNumber)) + } + if len(twilioColumns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( + twilioColumns, + []handler.Condition{ + handler.NewCond(SMSTwilioColumnSMSID, e.ID), + handler.NewCond(SMSTwilioColumnInstanceID, e.Aggregate().InstanceID), + }, + handler.WithTableSuffix(smsTwilioTableSuffix), + )) + } + + return handler.NewMultiStatement(e, stmts...), nil } func (p *smsConfigProjection) reduceSMSConfigTwilioTokenChanged(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigTwilioTokenChangedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-fi99F", "reduce.wrong.event.type %s", instance.SMSConfigTwilioTokenChangedEventType) - } - columns := make([]handler.Column, 0) - if e.Token != nil { - columns = append(columns, handler.NewCol(SMSTwilioConfigColumnToken, e.Token)) + e, err := assertEvent[*instance.SMSConfigTwilioTokenChangedEvent](event) + if err != nil { + return nil, err } return handler.NewMultiStatement( e, handler.AddUpdateStatement( - columns, + []handler.Column{ + handler.NewCol(SMSTwilioColumnToken, e.Token), + }, []handler.Condition{ - handler.NewCond(SMSTwilioConfigColumnSMSID, e.ID), + handler.NewCond(SMSTwilioColumnSMSID, e.ID), handler.NewCond(SMSTwilioColumnInstanceID, e.Aggregate().InstanceID), }, handler.WithTableSuffix(smsTwilioTableSuffix), @@ -210,15 +254,122 @@ func (p *smsConfigProjection) reduceSMSConfigTwilioTokenChanged(event eventstore ), nil } -func (p *smsConfigProjection) reduceSMSConfigActivated(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigActivatedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-fj9Ef", "reduce.wrong.event.type %s", instance.SMSConfigActivatedEventType) +func (p *smsConfigProjection) reduceSMSConfigHTTPAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigHTTPAddedEvent](event) + if err != nil { + return nil, err } + + return handler.NewMultiStatement( + e, + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(SMSColumnID, e.ID), + handler.NewCol(SMSColumnAggregateID, e.Aggregate().ID), + handler.NewCol(SMSColumnCreationDate, e.CreationDate()), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(SMSColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), + handler.NewCol(SMSColumnSequence, e.Sequence()), + handler.NewCol(SMSColumnDescription, e.Description), + }, + ), + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(SMSHTTPColumnSMSID, e.ID), + handler.NewCol(SMSHTTPColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMSHTTPColumnEndpoint, e.Endpoint), + }, + handler.WithTableSuffix(smsHTTPTableSuffix), + ), + ), nil +} + +func (p *smsConfigProjection) reduceSMSConfigHTTPChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigHTTPChangedEvent](event) + if err != nil { + return nil, err + } + + stmts := make([]func(eventstore.Event) handler.Exec, 0, 3) + columns := []handler.Column{ + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + } + if e.Description != nil { + columns = append(columns, handler.NewCol(SMSColumnDescription, *e.Description)) + } + if len(columns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( + columns, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + )) + } + + if e.Endpoint != nil { + stmts = append(stmts, handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSHTTPColumnEndpoint, *e.Endpoint), + }, + []handler.Condition{ + handler.NewCond(SMSHTTPColumnSMSID, e.ID), + handler.NewCond(SMSHTTPColumnInstanceID, e.Aggregate().InstanceID), + }, + handler.WithTableSuffix(smsHTTPTableSuffix), + )) + } + + return handler.NewMultiStatement(e, stmts...), nil +} + +func (p *smsConfigProjection) reduceSMSConfigTwilioActivated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigTwilioActivatedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewMultiStatement( + e, + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.Not(handler.NewCond(SMSColumnID, e.ID)), + handler.NewCond(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + ), nil +} + +func (p *smsConfigProjection) reduceSMSConfigTwilioDeactivated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigTwilioDeactivatedEvent](event) + if err != nil { + return nil, err + } + return handler.NewUpdateStatement( e, []handler.Column{ - handler.NewCol(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), handler.NewCol(SMSColumnChangeDate, e.CreationDate()), handler.NewCol(SMSColumnSequence, e.Sequence()), }, @@ -229,11 +380,61 @@ func (p *smsConfigProjection) reduceSMSConfigActivated(event eventstore.Event) ( ), nil } -func (p *smsConfigProjection) reduceSMSConfigDeactivated(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigDeactivatedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-dj9Js", "reduce.wrong.event.type %s", instance.SMSConfigDeactivatedEventType) +func (p *smsConfigProjection) reduceSMSConfigTwilioRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigTwilioRemovedEvent](event) + if err != nil { + return nil, err } + + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), nil +} + +func (p *smsConfigProjection) reduceSMSConfigActivated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigActivatedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewMultiStatement( + e, + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.Not(handler.NewCond(SMSColumnID, e.ID)), + handler.NewCond(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + ), nil +} + +func (p *smsConfigProjection) reduceSMSConfigDeactivated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigDeactivatedEvent](event) + if err != nil { + return nil, err + } + return handler.NewUpdateStatement( e, []handler.Column{ @@ -249,10 +450,11 @@ func (p *smsConfigProjection) reduceSMSConfigDeactivated(event eventstore.Event) } func (p *smsConfigProjection) reduceSMSConfigRemoved(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigRemovedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-s9JJf", "reduce.wrong.event.type %s", instance.SMSConfigRemovedEventType) + e, err := assertEvent[*instance.SMSConfigRemovedEvent](event) + if err != nil { + return nil, err } + return handler.NewDeleteStatement( e, []handler.Condition{ diff --git a/internal/query/projection/sms_test.go b/internal/query/projection/sms_test.go index 1e1a384ed..88ce6e441 100644 --- a/internal/query/projection/sms_test.go +++ b/internal/query/projection/sms_test.go @@ -37,9 +37,10 @@ func TestSMSProjection_reduces(t *testing.T) { "keyId": "key-id", "crypted": "Y3J5cHRlZA==" }, - "senderNumber": "sender-number" + "senderNumber": "sender-number", + "description": "description" }`), - ), instance.SMSConfigTwilioAddedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioAddedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioAdded, want: wantReduce{ @@ -48,7 +49,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.sms_configs2 (id, aggregate_id, creation_date, change_date, resource_owner, instance_id, state, sequence) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedStmt: "INSERT INTO projections.sms_configs3 (id, aggregate_id, creation_date, change_date, resource_owner, instance_id, state, sequence, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "id", "agg-id", @@ -58,10 +59,11 @@ func TestSMSProjection_reduces(t *testing.T) { "instance-id", domain.SMSConfigStateInactive, uint64(15), + "description", }, }, { - expectedStmt: "INSERT INTO projections.sms_configs2_twilio (sms_id, instance_id, sid, token, sender_number) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.sms_configs3_twilio (sms_id, instance_id, sid, token, sender_number) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "id", "instance-id", @@ -89,9 +91,10 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id", "sid": "sid", - "senderNumber": "sender-number" + "senderNumber": "sender-number", + "description": "description" }`), - ), instance.SMSConfigTwilioChangedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioChangedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioChanged, want: wantReduce{ @@ -100,7 +103,17 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2_twilio SET (sid, sender_number) = ($1, $2) WHERE (sms_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "description", + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_twilio SET (sid, sender_number) = ($1, $2) WHERE (sms_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "sid", "sender-number", @@ -108,11 +121,75 @@ func TestSMSProjection_reduces(t *testing.T) { "instance-id", }, }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioChanged, only description", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "description": "description" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), + "description", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioChanged, only sid", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "sid": "sid" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_twilio SET sid = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "sid", "id", "instance-id", }, @@ -137,7 +214,7 @@ func TestSMSProjection_reduces(t *testing.T) { "crypted": "Y3J5cHRlZA==" } }`), - ), instance.SMSConfigTwilioTokenChangedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioTokenChangedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioTokenChanged, want: wantReduce{ @@ -146,7 +223,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2_twilio SET token = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.sms_configs3_twilio SET token = $1 WHERE (sms_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -159,7 +236,7 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.sms_configs2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -171,6 +248,270 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, }, + { + name: "instance reduceSMSHTTPAdded", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPAddedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "description": "description", + "endpoint": "endpoint" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPAddedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.sms_configs3 (id, aggregate_id, creation_date, change_date, resource_owner, instance_id, state, sequence, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "id", + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + "instance-id", + domain.SMSConfigStateInactive, + uint64(15), + "description", + }, + }, + { + expectedStmt: "INSERT INTO projections.sms_configs3_http (sms_id, instance_id, endpoint) VALUES ($1, $2, $3)", + expectedArgs: []interface{}{ + "id", + "instance-id", + "endpoint", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigHTTPChanged", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "endpoint": "endpoint", + "description": "description" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "description", + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_http SET endpoint = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "endpoint", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigHTTPChanged, only description", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "description": "description" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "description", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, { + name: "instance reduceSMSConfigHTTPChanged, only endpoint", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "endpoint": "endpoint" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_http SET endpoint = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "endpoint", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioActivated", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioActivatedEventType, + instance.AggregateType, + []byte(`{ + "id": "id" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioActivatedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioActivated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", + expectedArgs: []interface{}{ + domain.SMSConfigStateInactive, + anyArg{}, + uint64(15), + "id", + domain.SMSConfigStateActive, + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + domain.SMSConfigStateActive, + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioDeactivated", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioDeactivatedEventType, + instance.AggregateType, + []byte(`{ + "id": "id" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioDeactivatedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioDeactivated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + domain.SMSConfigStateInactive, + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioRemoved", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioRemovedEventType, + instance.AggregateType, + []byte(`{ + "id": "id" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioRemovedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.sms_configs3 WHERE (id = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "id", + "instance-id", + }, + }, + }, + }, + }, + }, { name: "instance reduceSMSConfigActivated", args: args{ @@ -181,7 +522,7 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id" }`), - ), instance.SMSConfigActivatedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigActivatedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigActivated, want: wantReduce{ @@ -190,7 +531,18 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", + expectedArgs: []interface{}{ + domain.SMSConfigStateInactive, + anyArg{}, + uint64(15), + "id", + domain.SMSConfigStateActive, + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ domain.SMSConfigStateActive, anyArg{}, @@ -213,7 +565,7 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id" }`), - ), instance.SMSConfigDeactivatedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigDeactivatedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigDeactivated, want: wantReduce{ @@ -222,7 +574,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ domain.SMSConfigStateInactive, anyArg{}, @@ -245,7 +597,7 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id" }`), - ), instance.SMSConfigRemovedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigRemovedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigRemoved, want: wantReduce{ @@ -254,7 +606,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sms_configs2 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.sms_configs3 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "id", "instance-id", @@ -281,7 +633,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sms_configs2 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.sms_configs3 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/sms.go b/internal/query/sms.go index 38c798ba1..7aa4be231 100644 --- a/internal/query/sms.go +++ b/internal/query/sms.go @@ -30,8 +30,10 @@ type SMSConfig struct { ResourceOwner string State domain.SMSConfigState Sequence uint64 + Description string TwilioConfig *Twilio + HTTPConfig *HTTP } type Twilio struct { @@ -40,6 +42,10 @@ type Twilio struct { SenderNumber string } +type HTTP struct { + Endpoint string +} + type SMSConfigsSearchQueries struct { SearchRequest Queries []SearchQuery @@ -58,60 +64,79 @@ var ( name: projection.SMSConfigProjectionTable, instanceIDCol: projection.SMSColumnInstanceID, } - SMSConfigColumnID = Column{ + SMSColumnID = Column{ name: projection.SMSColumnID, table: smsConfigsTable, } - SMSConfigColumnAggregateID = Column{ + SMSColumnAggregateID = Column{ name: projection.SMSColumnAggregateID, table: smsConfigsTable, } - SMSConfigColumnCreationDate = Column{ + SMSColumnCreationDate = Column{ name: projection.SMSColumnCreationDate, table: smsConfigsTable, } - SMSConfigColumnChangeDate = Column{ + SMSColumnChangeDate = Column{ name: projection.SMSColumnChangeDate, table: smsConfigsTable, } - SMSConfigColumnResourceOwner = Column{ + SMSColumnResourceOwner = Column{ name: projection.SMSColumnResourceOwner, table: smsConfigsTable, } - SMSConfigColumnInstanceID = Column{ + SMSColumnInstanceID = Column{ name: projection.SMSColumnInstanceID, table: smsConfigsTable, } - SMSConfigColumnState = Column{ + SMSColumnState = Column{ name: projection.SMSColumnState, table: smsConfigsTable, } - SMSConfigColumnSequence = Column{ + SMSColumnSequence = Column{ name: projection.SMSColumnSequence, table: smsConfigsTable, } + SMSColumnDescription = Column{ + name: projection.SMSColumnDescription, + table: smsConfigsTable, + } ) var ( - smsTwilioConfigsTable = table{ + smsTwilioTable = table{ name: projection.SMSTwilioTable, instanceIDCol: projection.SMSTwilioColumnInstanceID, } - SMSTwilioConfigColumnSMSID = Column{ - name: projection.SMSTwilioConfigColumnSMSID, - table: smsTwilioConfigsTable, + SMSTwilioColumnSMSID = Column{ + name: projection.SMSTwilioColumnSMSID, + table: smsTwilioTable, } - SMSTwilioConfigColumnSID = Column{ - name: projection.SMSTwilioConfigColumnSID, - table: smsTwilioConfigsTable, + SMSTwilioColumnSID = Column{ + name: projection.SMSTwilioColumnSID, + table: smsTwilioTable, } - SMSTwilioConfigColumnToken = Column{ - name: projection.SMSTwilioConfigColumnToken, - table: smsTwilioConfigsTable, + SMSTwilioColumnToken = Column{ + name: projection.SMSTwilioColumnToken, + table: smsTwilioTable, } - SMSTwilioConfigColumnSenderNumber = Column{ - name: projection.SMSTwilioConfigColumnSenderNumber, - table: smsTwilioConfigsTable, + SMSTwilioColumnSenderNumber = Column{ + name: projection.SMSTwilioColumnSenderNumber, + table: smsTwilioTable, + } +) + +var ( + smsHTTPTable = table{ + name: projection.SMSHTTPTable, + instanceIDCol: projection.SMSHTTPColumnInstanceID, + } + SMSHTTPColumnSMSID = Column{ + name: projection.SMSHTTPColumnSMSID, + table: smsHTTPTable, + } + SMSHTTPColumnEndpoint = Column{ + name: projection.SMSHTTPColumnEndpoint, + table: smsHTTPTable, } ) @@ -122,8 +147,8 @@ func (q *Queries) SMSProviderConfigByID(ctx context.Context, id string) (config query, scan := prepareSMSConfigQuery(ctx, q.client) stmt, args, err := query.Where( sq.Eq{ - SMSConfigColumnID.identifier(): id, - SMSConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + SMSColumnID.identifier(): id, + SMSColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }, ).ToSql() if err != nil { @@ -137,17 +162,15 @@ func (q *Queries) SMSProviderConfigByID(ctx context.Context, id string) (config return config, err } -func (q *Queries) SMSProviderConfig(ctx context.Context, queries ...SearchQuery) (config *SMSConfig, err error) { +func (q *Queries) SMSProviderConfigActive(ctx context.Context, instanceID string) (config *SMSConfig, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareSMSConfigQuery(ctx, q.client) - for _, searchQuery := range queries { - query = searchQuery.toQuery(query) - } stmt, args, err := query.Where( sq.Eq{ - SMSConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + SMSColumnInstanceID.identifier(): instanceID, + SMSColumnState.identifier(): domain.SMSConfigStateActive, }, ).ToSql() if err != nil { @@ -168,7 +191,7 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc query, scan := prepareSMSConfigsQuery(ctx, q.client) stmt, args, err := queries.toQuery(query). Where(sq.Eq{ - SMSConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + SMSColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-sn9Jf", "Errors.Query.InvalidRequest") @@ -186,30 +209,36 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc } func NewSMSProviderStateQuery(state domain.SMSConfigState) (SearchQuery, error) { - return NewNumberQuery(SMSConfigColumnState, state, NumberEquals) + return NewNumberQuery(SMSColumnState, state, NumberEquals) } func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SMSConfig, error)) { return sq.Select( - SMSConfigColumnID.identifier(), - SMSConfigColumnAggregateID.identifier(), - SMSConfigColumnCreationDate.identifier(), - SMSConfigColumnChangeDate.identifier(), - SMSConfigColumnResourceOwner.identifier(), - SMSConfigColumnState.identifier(), - SMSConfigColumnSequence.identifier(), + SMSColumnID.identifier(), + SMSColumnAggregateID.identifier(), + SMSColumnCreationDate.identifier(), + SMSColumnChangeDate.identifier(), + SMSColumnResourceOwner.identifier(), + SMSColumnState.identifier(), + SMSColumnSequence.identifier(), + SMSColumnDescription.identifier(), - SMSTwilioConfigColumnSMSID.identifier(), - SMSTwilioConfigColumnSID.identifier(), - SMSTwilioConfigColumnToken.identifier(), - SMSTwilioConfigColumnSenderNumber.identifier(), + SMSTwilioColumnSMSID.identifier(), + SMSTwilioColumnSID.identifier(), + SMSTwilioColumnToken.identifier(), + SMSTwilioColumnSenderNumber.identifier(), + + SMSHTTPColumnSMSID.identifier(), + SMSHTTPColumnEndpoint.identifier(), ).From(smsConfigsTable.identifier()). - LeftJoin(join(SMSTwilioConfigColumnSMSID, SMSConfigColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). + LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SMSConfig, error) { config := new(SMSConfig) var ( twilioConfig = sqlTwilioConfig{} + httpConfig = sqlHTTPConfig{} ) err := row.Scan( @@ -220,11 +249,15 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu &config.ResourceOwner, &config.State, &config.Sequence, + &config.Description, &twilioConfig.smsID, &twilioConfig.sid, &twilioConfig.token, &twilioConfig.senderNumber, + + &httpConfig.smsID, + &httpConfig.endpoint, ) if err != nil { @@ -235,6 +268,7 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } twilioConfig.set(config) + httpConfig.set(config) return config, nil } @@ -242,21 +276,27 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*SMSConfigs, error)) { return sq.Select( - SMSConfigColumnID.identifier(), - SMSConfigColumnAggregateID.identifier(), - SMSConfigColumnCreationDate.identifier(), - SMSConfigColumnChangeDate.identifier(), - SMSConfigColumnResourceOwner.identifier(), - SMSConfigColumnState.identifier(), - SMSConfigColumnSequence.identifier(), + SMSColumnID.identifier(), + SMSColumnAggregateID.identifier(), + SMSColumnCreationDate.identifier(), + SMSColumnChangeDate.identifier(), + SMSColumnResourceOwner.identifier(), + SMSColumnState.identifier(), + SMSColumnSequence.identifier(), + SMSColumnDescription.identifier(), + + SMSTwilioColumnSMSID.identifier(), + SMSTwilioColumnSID.identifier(), + SMSTwilioColumnToken.identifier(), + SMSTwilioColumnSenderNumber.identifier(), + + SMSHTTPColumnSMSID.identifier(), + SMSHTTPColumnEndpoint.identifier(), - SMSTwilioConfigColumnSMSID.identifier(), - SMSTwilioConfigColumnSID.identifier(), - SMSTwilioConfigColumnToken.identifier(), - SMSTwilioConfigColumnSenderNumber.identifier(), countColumn.identifier(), ).From(smsConfigsTable.identifier()). - LeftJoin(join(SMSTwilioConfigColumnSMSID, SMSConfigColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). + LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Rows) (*SMSConfigs, error) { configs := &SMSConfigs{Configs: []*SMSConfig{}} @@ -264,6 +304,7 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB config := new(SMSConfig) var ( twilioConfig = sqlTwilioConfig{} + httpConfig = sqlHTTPConfig{} ) err := row.Scan( @@ -274,11 +315,16 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB &config.ResourceOwner, &config.State, &config.Sequence, + &config.Description, &twilioConfig.smsID, &twilioConfig.sid, &twilioConfig.token, &twilioConfig.senderNumber, + + &httpConfig.smsID, + &httpConfig.endpoint, + &configs.Count, ) @@ -287,6 +333,7 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } twilioConfig.set(config) + httpConfig.set(config) configs.Configs = append(configs.Configs, config) } @@ -312,3 +359,17 @@ func (c sqlTwilioConfig) set(smsConfig *SMSConfig) { SenderNumber: c.senderNumber.String, } } + +type sqlHTTPConfig struct { + smsID sql.NullString + endpoint sql.NullString +} + +func (c sqlHTTPConfig) set(smsConfig *SMSConfig) { + if !c.smsID.Valid { + return + } + smsConfig.HTTPConfig = &HTTP{ + Endpoint: c.endpoint.String, + } +} diff --git a/internal/query/sms_test.go b/internal/query/sms_test.go index 56bb97e2e..20cf62f8c 100644 --- a/internal/query/sms_test.go +++ b/internal/query/sms_test.go @@ -14,38 +14,50 @@ import ( ) var ( - expectedSMSConfigQuery = regexp.QuoteMeta(`SELECT projections.sms_configs2.id,` + - ` projections.sms_configs2.aggregate_id,` + - ` projections.sms_configs2.creation_date,` + - ` projections.sms_configs2.change_date,` + - ` projections.sms_configs2.resource_owner,` + - ` projections.sms_configs2.state,` + - ` projections.sms_configs2.sequence,` + + expectedSMSConfigQuery = regexp.QuoteMeta(`SELECT projections.sms_configs3.id,` + + ` projections.sms_configs3.aggregate_id,` + + ` projections.sms_configs3.creation_date,` + + ` projections.sms_configs3.change_date,` + + ` projections.sms_configs3.resource_owner,` + + ` projections.sms_configs3.state,` + + ` projections.sms_configs3.sequence,` + + ` projections.sms_configs3.description,` + // twilio config - ` projections.sms_configs2_twilio.sms_id,` + - ` projections.sms_configs2_twilio.sid,` + - ` projections.sms_configs2_twilio.token,` + - ` projections.sms_configs2_twilio.sender_number` + - ` FROM projections.sms_configs2` + - ` LEFT JOIN projections.sms_configs2_twilio ON projections.sms_configs2.id = projections.sms_configs2_twilio.sms_id AND projections.sms_configs2.instance_id = projections.sms_configs2_twilio.instance_id` + + ` projections.sms_configs3_twilio.sms_id,` + + ` projections.sms_configs3_twilio.sid,` + + ` projections.sms_configs3_twilio.token,` + + ` projections.sms_configs3_twilio.sender_number,` + + + // http config + ` projections.sms_configs3_http.sms_id,` + + ` projections.sms_configs3_http.endpoint` + + ` FROM projections.sms_configs3` + + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) - expectedSMSConfigsQuery = regexp.QuoteMeta(`SELECT projections.sms_configs2.id,` + - ` projections.sms_configs2.aggregate_id,` + - ` projections.sms_configs2.creation_date,` + - ` projections.sms_configs2.change_date,` + - ` projections.sms_configs2.resource_owner,` + - ` projections.sms_configs2.state,` + - ` projections.sms_configs2.sequence,` + + expectedSMSConfigsQuery = regexp.QuoteMeta(`SELECT projections.sms_configs3.id,` + + ` projections.sms_configs3.aggregate_id,` + + ` projections.sms_configs3.creation_date,` + + ` projections.sms_configs3.change_date,` + + ` projections.sms_configs3.resource_owner,` + + ` projections.sms_configs3.state,` + + ` projections.sms_configs3.sequence,` + + ` projections.sms_configs3.description,` + // twilio config - ` projections.sms_configs2_twilio.sms_id,` + - ` projections.sms_configs2_twilio.sid,` + - ` projections.sms_configs2_twilio.token,` + - ` projections.sms_configs2_twilio.sender_number,` + + ` projections.sms_configs3_twilio.sms_id,` + + ` projections.sms_configs3_twilio.sid,` + + ` projections.sms_configs3_twilio.token,` + + ` projections.sms_configs3_twilio.sender_number,` + + + // http config + ` projections.sms_configs3_http.sms_id,` + + ` projections.sms_configs3_http.endpoint,` + ` COUNT(*) OVER ()` + - ` FROM projections.sms_configs2` + - ` LEFT JOIN projections.sms_configs2_twilio ON projections.sms_configs2.id = projections.sms_configs2_twilio.sms_id AND projections.sms_configs2.instance_id = projections.sms_configs2_twilio.instance_id` + + ` FROM projections.sms_configs3` + + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) smsConfigCols = []string{ @@ -56,16 +68,20 @@ var ( "resource_owner", "state", "sequence", + "description", // twilio config "sms_id", "sid", "token", "sender-number", + // http config + "sms_id", + "endpoint", } smsConfigsCols = append(smsConfigCols, "count") ) -func Test_SMSConfigssPrepare(t *testing.T) { +func Test_SMSConfigsPrepare(t *testing.T) { type want struct { sqlExpectations sqlExpectation err checkErr @@ -104,11 +120,15 @@ func Test_SMSConfigssPrepare(t *testing.T) { "ro", domain.SMSConfigStateInactive, uint64(20211109), + "description", // twilio config "sms-id", "sid", &crypto.CryptoValue{}, "sender-number", + // http config + nil, + nil, }, }, ), @@ -126,6 +146,7 @@ func Test_SMSConfigssPrepare(t *testing.T) { ResourceOwner: "ro", State: domain.SMSConfigStateInactive, Sequence: 20211109, + Description: "description", TwilioConfig: &Twilio{ SID: "sid", Token: &crypto.CryptoValue{}, @@ -135,6 +156,56 @@ func Test_SMSConfigssPrepare(t *testing.T) { }, }, }, + { + name: "prepareSMSQuery http config", + prepare: prepareSMSConfigsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedSMSConfigsQuery, + smsConfigsCols, + [][]driver.Value{ + { + "sms-id", + "agg-id", + testNow, + testNow, + "ro", + domain.SMSConfigStateInactive, + uint64(20211109), + "description", + // twilio config + nil, + nil, + nil, + nil, + // http config + "sms-id", + "endpoint", + }, + }, + ), + }, + object: &SMSConfigs{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Configs: []*SMSConfig{ + { + ID: "sms-id", + AggregateID: "agg-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.SMSConfigStateInactive, + Sequence: 20211109, + Description: "description", + HTTPConfig: &HTTP{ + Endpoint: "endpoint", + }, + }, + }, + }, + }, { name: "prepareSMSConfigsQuery multiple result", prepare: prepareSMSConfigsQuery, @@ -149,13 +220,17 @@ func Test_SMSConfigssPrepare(t *testing.T) { testNow, testNow, "ro", - domain.SMSConfigStateInactive, + domain.SMSConfigStateActive, uint64(20211109), + "description", // twilio config "sms-id", "sid", &crypto.CryptoValue{}, "sender-number", + // http config + nil, + nil, }, { "sms-id2", @@ -165,18 +240,40 @@ func Test_SMSConfigssPrepare(t *testing.T) { "ro", domain.SMSConfigStateInactive, uint64(20211109), + "description", // twilio config "sms-id2", "sid2", &crypto.CryptoValue{}, "sender-number2", + // http config + nil, + nil, + }, + { + "sms-id3", + "agg-id", + testNow, + testNow, + "ro", + domain.SMSConfigStateInactive, + uint64(20211109), + "description", + // twilio config + nil, + nil, + nil, + nil, + // http config + "sms-id3", + "endpoint3", }, }, ), }, object: &SMSConfigs{ SearchResponse: SearchResponse{ - Count: 2, + Count: 3, }, Configs: []*SMSConfig{ { @@ -185,8 +282,9 @@ func Test_SMSConfigssPrepare(t *testing.T) { CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", - State: domain.SMSConfigStateInactive, + State: domain.SMSConfigStateActive, Sequence: 20211109, + Description: "description", TwilioConfig: &Twilio{ SID: "sid", Token: &crypto.CryptoValue{}, @@ -201,12 +299,26 @@ func Test_SMSConfigssPrepare(t *testing.T) { ResourceOwner: "ro", State: domain.SMSConfigStateInactive, Sequence: 20211109, + Description: "description", TwilioConfig: &Twilio{ SID: "sid2", Token: &crypto.CryptoValue{}, SenderNumber: "sender-number2", }, }, + { + ID: "sms-id3", + AggregateID: "agg-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.SMSConfigStateInactive, + Sequence: 20211109, + Description: "description", + HTTPConfig: &HTTP{ + Endpoint: "endpoint3", + }, + }, }, }, }, @@ -265,7 +377,50 @@ func Test_SMSConfigPrepare(t *testing.T) { object: (*SMSConfig)(nil), }, { - name: "prepareSMSConfigQuery found", + name: "prepareSMSConfigQuery, twilio, found", + prepare: prepareSMSConfigQuery, + want: want{ + sqlExpectations: mockQuery( + expectedSMSConfigQuery, + smsConfigCols, + []driver.Value{ + "sms-id", + "agg-id", + testNow, + testNow, + "ro", + domain.SMSConfigStateActive, + uint64(20211109), + "description", + // twilio config + "sms-id", + "sid", + &crypto.CryptoValue{}, + "sender-number", + // http config + nil, + nil, + }, + ), + }, + object: &SMSConfig{ + ID: "sms-id", + AggregateID: "agg-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.SMSConfigStateActive, + Sequence: 20211109, + Description: "description", + TwilioConfig: &Twilio{ + SID: "sid", + SenderNumber: "sender-number", + Token: &crypto.CryptoValue{}, + }, + }, + }, + { + name: "prepareSMSConfigQuery, http, found", prepare: prepareSMSConfigQuery, want: want{ sqlExpectations: mockQuery( @@ -279,11 +434,15 @@ func Test_SMSConfigPrepare(t *testing.T) { "ro", domain.SMSConfigStateInactive, uint64(20211109), + "description", // twilio config + nil, + nil, + nil, + nil, + // http config "sms-id", - "sid", - &crypto.CryptoValue{}, - "sender-number", + "endpoint", }, ), }, @@ -295,10 +454,9 @@ func Test_SMSConfigPrepare(t *testing.T) { ResourceOwner: "ro", State: domain.SMSConfigStateInactive, Sequence: 20211109, - TwilioConfig: &Twilio{ - SID: "sid", - SenderNumber: "sender-number", - Token: &crypto.CryptoValue{}, + Description: "description", + HTTPConfig: &HTTP{ + Endpoint: "endpoint", }, }, }, diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 16b7e3967..d88793a39 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -18,12 +18,17 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigDeactivatedEventType, SMTPConfigDeactivatedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigPasswordChangedEventType, SMTPConfigPasswordChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigRemovedEventType, SMTPConfigRemovedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioAddedEventType, SMSConfigTwilioAddedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioChangedEventType, SMSConfigTwilioChangedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioTokenChangedEventType, SMSConfigTwilioTokenChangedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigActivatedEventType, SMSConfigActivatedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigDeactivatedEventType, SMSConfigDeactivatedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigRemovedEventType, SMSConfigRemovedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioAddedEventType, eventstore.GenericEventMapper[SMSConfigTwilioAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioChangedEventType, eventstore.GenericEventMapper[SMSConfigTwilioChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioTokenChangedEventType, eventstore.GenericEventMapper[SMSConfigTwilioTokenChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigHTTPAddedEventType, eventstore.GenericEventMapper[SMSConfigHTTPAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigHTTPChangedEventType, eventstore.GenericEventMapper[SMSConfigHTTPChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioActivatedEventType, eventstore.GenericEventMapper[SMSConfigTwilioActivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioDeactivatedEventType, eventstore.GenericEventMapper[SMSConfigTwilioDeactivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioRemovedEventType, eventstore.GenericEventMapper[SMSConfigTwilioRemovedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigActivatedEventType, eventstore.GenericEventMapper[SMSConfigActivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigDeactivatedEventType, eventstore.GenericEventMapper[SMSConfigDeactivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigRemovedEventType, eventstore.GenericEventMapper[SMSConfigRemovedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, DebugNotificationProviderFileAddedEventType, DebugNotificationProviderFileAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, DebugNotificationProviderFileChangedEventType, DebugNotificationProviderFileChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, DebugNotificationProviderFileRemovedEventType, DebugNotificationProviderFileRemovedEventMapper) diff --git a/internal/repository/instance/sms.go b/internal/repository/instance/sms.go index 1b79f1d77..309ce9aa4 100644 --- a/internal/repository/instance/sms.go +++ b/internal/repository/instance/sms.go @@ -11,18 +11,25 @@ import ( const ( smsConfigPrefix = "sms.config" smsConfigTwilioPrefix = "twilio." + smsConfigHTTPPrefix = "http." SMSConfigTwilioAddedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "added" SMSConfigTwilioChangedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "changed" + SMSConfigHTTPAddedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigHTTPPrefix + "added" + SMSConfigHTTPChangedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigHTTPPrefix + "changed" SMSConfigTwilioTokenChangedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "token.changed" - SMSConfigActivatedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "activated" - SMSConfigDeactivatedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "deactivated" - SMSConfigRemovedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "removed" + SMSConfigTwilioActivatedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "activated" + SMSConfigTwilioDeactivatedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "deactivated" + SMSConfigTwilioRemovedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "removed" + SMSConfigActivatedEventType = instanceEventTypePrefix + smsConfigPrefix + "activated" + SMSConfigDeactivatedEventType = instanceEventTypePrefix + smsConfigPrefix + "deactivated" + SMSConfigRemovedEventType = instanceEventTypePrefix + smsConfigPrefix + "removed" ) type SMSConfigTwilioAddedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` SID string `json:"sid,omitempty"` Token *crypto.CryptoValue `json:"token,omitempty"` SenderNumber string `json:"senderNumber,omitempty"` @@ -32,23 +39,29 @@ func NewSMSConfigTwilioAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, id, + description string, sid, senderNumber string, token *crypto.CryptoValue, ) *SMSConfigTwilioAddedEvent { return &SMSConfigTwilioAddedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigTwilioAddedEventType, ), ID: id, + Description: description, SID: sid, Token: token, SenderNumber: senderNumber, } } +func (e *SMSConfigTwilioAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigTwilioAddedEvent) Payload() interface{} { return e } @@ -57,22 +70,11 @@ func (e *SMSConfigTwilioAddedEvent) UniqueConstraints() []*eventstore.UniqueCons return nil } -func SMSConfigTwilioAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigAdded := &SMSConfigTwilioAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigAdded) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-smwiR", "unable to unmarshal sms config twilio added") - } - - return smsConfigAdded, nil -} - type SMSConfigTwilioChangedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` SID *string `json:"sid,omitempty"` SenderNumber *string `json:"senderNumber,omitempty"` } @@ -87,7 +89,7 @@ func NewSMSConfigTwilioChangedEvent( return nil, zerrors.ThrowPreconditionFailed(nil, "IAM-smn8e", "Errors.NoChangesFound") } changeEvent := &SMSConfigTwilioChangedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigTwilioChangedEventType, @@ -108,12 +110,22 @@ func ChangeSMSConfigTwilioSID(sid string) func(event *SMSConfigTwilioChangedEven } } +func ChangeSMSConfigTwilioDescription(description string) func(event *SMSConfigTwilioChangedEvent) { + return func(e *SMSConfigTwilioChangedEvent) { + e.Description = &description + } +} + func ChangeSMSConfigTwilioSenderNumber(senderNumber string) func(event *SMSConfigTwilioChangedEvent) { return func(e *SMSConfigTwilioChangedEvent) { e.SenderNumber = &senderNumber } } +func (e *SMSConfigTwilioChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigTwilioChangedEvent) Payload() interface{} { return e } @@ -122,20 +134,8 @@ func (e *SMSConfigTwilioChangedEvent) UniqueConstraints() []*eventstore.UniqueCo return nil } -func SMSConfigTwilioChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigChanged := &SMSConfigTwilioChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigChanged) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-smwiR", "unable to unmarshal sms config twilio added") - } - - return smsConfigChanged, nil -} - type SMSConfigTwilioTokenChangedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` ID string `json:"id,omitempty"` Token *crypto.CryptoValue `json:"token,omitempty"` @@ -148,7 +148,7 @@ func NewSMSConfigTokenChangedEvent( token *crypto.CryptoValue, ) *SMSConfigTwilioTokenChangedEvent { return &SMSConfigTwilioTokenChangedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigTwilioTokenChangedEventType, @@ -158,6 +158,10 @@ func NewSMSConfigTokenChangedEvent( } } +func (e *SMSConfigTwilioTokenChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigTwilioTokenChangedEvent) Payload() interface{} { return e } @@ -166,30 +170,130 @@ func (e *SMSConfigTwilioTokenChangedEvent) UniqueConstraints() []*eventstore.Uni return nil } -func SMSConfigTwilioTokenChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smtpConfigTokenChagned := &SMSConfigTwilioTokenChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smtpConfigTokenChagned) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-fi9Wf", "unable to unmarshal sms config token changed") - } +type SMSConfigHTTPAddedEvent struct { + *eventstore.BaseEvent `json:"-"` - return smtpConfigTokenChagned, nil + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Endpoint string `json:"endpoint,omitempty"` +} + +func NewSMSConfigHTTPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + description, + endpoint string, +) *SMSConfigHTTPAddedEvent { + return &SMSConfigHTTPAddedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMSConfigHTTPAddedEventType, + ), + ID: id, + Description: description, + Endpoint: endpoint, + } +} + +func (e *SMSConfigHTTPAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigHTTPAddedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigHTTPAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +type SMSConfigHTTPChangedEvent struct { + *eventstore.BaseEvent `json:"-"` + + ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` + Endpoint *string `json:"endpoint,omitempty"` +} + +func NewSMSConfigHTTPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []SMSConfigHTTPChanges, +) (*SMSConfigHTTPChangedEvent, error) { + if len(changes) == 0 { + return nil, zerrors.ThrowPreconditionFailed(nil, "IAM-smn8e", "Errors.NoChangesFound") + } + changeEvent := &SMSConfigHTTPChangedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMSConfigHTTPChangedEventType, + ), + ID: id, + } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type SMSConfigHTTPChanges func(event *SMSConfigHTTPChangedEvent) + +func ChangeSMSConfigHTTPDescription(description string) func(event *SMSConfigHTTPChangedEvent) { + return func(e *SMSConfigHTTPChangedEvent) { + e.Description = &description + } +} +func ChangeSMSConfigHTTPEndpoint(endpoint string) func(event *SMSConfigHTTPChangedEvent) { + return func(e *SMSConfigHTTPChangedEvent) { + e.Endpoint = &endpoint + } +} + +func (e *SMSConfigHTTPChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigHTTPChangedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigHTTPChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +type SMSConfigTwilioActivatedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` +} + +func (e *SMSConfigTwilioActivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigTwilioActivatedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigTwilioActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil } type SMSConfigActivatedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } -func NewSMSConfigTwilioActivatedEvent( +func NewSMSConfigActivatedEvent( ctx context.Context, aggregate *eventstore.Aggregate, id string, ) *SMSConfigActivatedEvent { return &SMSConfigActivatedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigActivatedEventType, @@ -198,6 +302,10 @@ func NewSMSConfigTwilioActivatedEvent( } } +func (e *SMSConfigActivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigActivatedEvent) Payload() interface{} { return e } @@ -206,21 +314,26 @@ func (e *SMSConfigActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstr return nil } -func SMSConfigActivatedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigActivated := &SMSConfigActivatedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigActivated) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-dn92f", "unable to unmarshal sms config twilio activated changed") - } +type SMSConfigTwilioDeactivatedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` +} - return smsConfigActivated, nil +func (e *SMSConfigTwilioDeactivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigTwilioDeactivatedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigTwilioDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil } type SMSConfigDeactivatedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } func NewSMSConfigDeactivatedEvent( @@ -229,7 +342,7 @@ func NewSMSConfigDeactivatedEvent( id string, ) *SMSConfigDeactivatedEvent { return &SMSConfigDeactivatedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigDeactivatedEventType, @@ -238,6 +351,10 @@ func NewSMSConfigDeactivatedEvent( } } +func (e *SMSConfigDeactivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigDeactivatedEvent) Payload() interface{} { return e } @@ -246,21 +363,26 @@ func (e *SMSConfigDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueCons return nil } -func SMSConfigDeactivatedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigDeactivated := &SMSConfigDeactivatedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigDeactivated) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-dn92f", "unable to unmarshal sms config twilio deactivated changed") - } +type SMSConfigTwilioRemovedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` +} - return smsConfigDeactivated, nil +func (e *SMSConfigTwilioRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigTwilioRemovedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigTwilioRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil } type SMSConfigRemovedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } func NewSMSConfigRemovedEvent( @@ -269,7 +391,7 @@ func NewSMSConfigRemovedEvent( id string, ) *SMSConfigRemovedEvent { return &SMSConfigRemovedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigRemovedEventType, @@ -278,6 +400,10 @@ func NewSMSConfigRemovedEvent( } } +func (e *SMSConfigRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigRemovedEvent) Payload() interface{} { return e } @@ -285,15 +411,3 @@ func (e *SMSConfigRemovedEvent) Payload() interface{} { func (e *SMSConfigRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return nil } - -func SMSConfigRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigRemoved := &SMSConfigRemovedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigRemoved) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-99iNF", "unable to unmarshal sms config removed") - } - - return smsConfigRemoved, nil -} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 1db51d1b9..41de3f845 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -690,6 +690,40 @@ service AdminService { }; } + rpc AddSMSProviderHTTP(AddSMSProviderHTTPRequest) returns (AddSMSProviderHTTPResponse) { + option (google.api.http) = { + post: "/sms/http"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "SMS Provider"; + summary: "Add HTTP SMS Provider"; + description: "Configure a new SMS provider of the type HTTP. A provider has to be activated to be able to send notifications." + }; + } + + rpc UpdateSMSProviderHTTP(UpdateSMSProviderHTTPRequest) returns (UpdateSMSProviderHTTPResponse) { + option (google.api.http) = { + put: "/sms/http/{id}"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "SMS Provider"; + summary: "Update HTTP SMS Provider"; + description: "Change the configuration of an SMS provider of the type HTTP. A provider has to be activated to be able to send notifications." + }; + } + rpc ActivateSMSProvider(ActivateSMSProviderRequest) returns (ActivateSMSProviderResponse) { option (google.api.http) = { post: "/sms/{id}/_activate"; @@ -4507,6 +4541,14 @@ message AddSMSProviderTwilioRequest { max_length: 200; } ]; + string description = 4 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; } message AddSMSProviderTwilioResponse { @@ -4534,6 +4576,14 @@ message UpdateSMSProviderTwilioRequest { max_length: 200; } ]; + string description = 4 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; } message UpdateSMSProviderTwilioResponse { @@ -4549,6 +4599,56 @@ message UpdateSMSProviderTwilioTokenResponse { zitadel.v1.ObjectDetails details = 1; } +message AddSMSProviderHTTPRequest { + string endpoint = 1 [ + (validate.rules).string = {min_len: 1, max_len: 2048}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"http://relay.example.com/provider\""; + min_length: 1; + max_length: 2048; + } + ]; + string description = 2 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; +} + +message AddSMSProviderHTTPResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateSMSProviderHTTPRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string endpoint = 2 [ + (validate.rules).string = {min_len: 1, max_len: 2048}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"http://relay.example.com/provider\""; + min_length: 1; + max_length: 2048; + } + ]; + string description = 3 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; +} + +message UpdateSMSProviderHTTPResponse { + zitadel.v1.ObjectDetails details = 1; +} + message ActivateSMSProviderRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } diff --git a/proto/zitadel/settings.proto b/proto/zitadel/settings.proto index 18b343df0..7b4f656bf 100644 --- a/proto/zitadel/settings.proto +++ b/proto/zitadel/settings.proto @@ -7,7 +7,7 @@ import "protoc-gen-openapiv2/options/annotations.proto"; package zitadel.settings.v1; -option go_package ="github.com/zitadel/zitadel/pkg/grpc/settings"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings"; message SecretGenerator { SecretGeneratorType generator_type = 1; @@ -99,9 +99,11 @@ message SMSProvider { zitadel.v1.ObjectDetails details = 1; string id = 2; SMSProviderConfigState state = 3; + string description = 6; oneof config { TwilioConfig twilio = 4; + HTTPConfig http = 5; } } @@ -110,6 +112,10 @@ message TwilioConfig { string sender_number = 2; } +message HTTPConfig { + string endpoint = 1; +} + enum SMSProviderConfigState { SMS_PROVIDER_CONFIG_STATE_UNSPECIFIED = 0; SMS_PROVIDER_CONFIG_ACTIVE = 1; @@ -117,8 +123,8 @@ enum SMSProviderConfigState { } message DebugNotificationProvider { - zitadel.v1.ObjectDetails details = 1; - bool compact = 2; + zitadel.v1.ObjectDetails details = 1; + bool compact = 2; } message OIDCSettings {