mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-13 19:44:21 +00:00
14e2aba1bc
# Which Problems Are Solved Twilio supports a robust, multi-channel verification service that notably supports multi-region SMS sender numbers required for our use case. Currently, Zitadel does much of the work of the Twilio Verify (eg. localization, code generation, messaging) but doesn't support the pool of sender numbers that Twilio Verify does. # How the Problems Are Solved To support this API, we need to be able to store the Twilio Service ID and send that in a verification request where appropriate: phone number verification and SMS 2FA code paths. This PR does the following: - Adds the ability to use Twilio Verify of standard messaging through Twilio - Adds support for international numbers and more reliable verification messages sent from multiple numbers - Adds a new Twilio configuration option to support Twilio Verify in the admin console - Sends verification SMS messages through Twilio Verify - Implements Twilio Verification Checks for codes generated through the same # Additional Changes # Additional Context - base was implemented by @zhirschtritt in https://github.com/zitadel/zitadel/pull/8268 ❤️ - closes https://github.com/zitadel/zitadel/issues/8581 --------- Co-authored-by: Zachary Hirschtritt <zachary.hirschtritt@klaviyo.com> Co-authored-by: Joey Biscoglia <joey.biscoglia@klaviyo.com>
569 lines
16 KiB
Go
569 lines
16 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
)
|
|
|
|
type UserV2WriteModel struct {
|
|
eventstore.WriteModel
|
|
|
|
UserName string
|
|
|
|
MachineWriteModel bool
|
|
Name string
|
|
Description string
|
|
AccessTokenType domain.OIDCTokenType
|
|
|
|
MachineSecretWriteModel bool
|
|
ClientSecret *crypto.CryptoValue
|
|
|
|
ProfileWriteModel bool
|
|
FirstName string
|
|
LastName string
|
|
NickName string
|
|
DisplayName string
|
|
PreferredLanguage language.Tag
|
|
Gender domain.Gender
|
|
|
|
AvatarWriteModel bool
|
|
Avatar string
|
|
|
|
HumanWriteModel bool
|
|
InitCode *crypto.CryptoValue
|
|
InitCodeCreationDate time.Time
|
|
InitCodeExpiry time.Duration
|
|
InitCheckFailedCount uint64
|
|
|
|
PasswordWriteModel bool
|
|
PasswordEncodedHash string
|
|
PasswordChangeRequired bool
|
|
PasswordCode *crypto.CryptoValue
|
|
PasswordCodeCreationDate time.Time
|
|
PasswordCodeExpiry time.Duration
|
|
PasswordCheckFailedCount uint64
|
|
PasswordCodeGeneratorID string
|
|
PasswordCodeVerificationID string
|
|
|
|
EmailWriteModel bool
|
|
Email domain.EmailAddress
|
|
IsEmailVerified bool
|
|
EmailCode *crypto.CryptoValue
|
|
EmailCodeCreationDate time.Time
|
|
EmailCodeExpiry time.Duration
|
|
EmailCheckFailedCount uint64
|
|
|
|
PhoneWriteModel bool
|
|
Phone domain.PhoneNumber
|
|
IsPhoneVerified bool
|
|
PhoneCode *crypto.CryptoValue
|
|
PhoneCodeCreationDate time.Time
|
|
PhoneCodeExpiry time.Duration
|
|
PhoneCheckFailedCount uint64
|
|
|
|
StateWriteModel bool
|
|
UserState domain.UserState
|
|
|
|
IDPLinkWriteModel bool
|
|
IDPLinks []*domain.UserIDPLink
|
|
}
|
|
|
|
func NewUserExistsWriteModel(userID, resourceOwner string) *UserV2WriteModel {
|
|
return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine())
|
|
}
|
|
|
|
func NewUserStateWriteModel(userID, resourceOwner string) *UserV2WriteModel {
|
|
return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine(), WithState())
|
|
}
|
|
|
|
func NewUserRemoveWriteModel(userID, resourceOwner string) *UserV2WriteModel {
|
|
return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine(), WithState(), WithIDPLinks())
|
|
}
|
|
|
|
func NewUserHumanWriteModel(userID, resourceOwner string, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinks bool) *UserV2WriteModel {
|
|
opts := []UserV2WMOption{WithHuman(), WithState()}
|
|
if profileWM {
|
|
opts = append(opts, WithProfile())
|
|
}
|
|
if emailWM {
|
|
opts = append(opts, WithEmail())
|
|
}
|
|
if phoneWM {
|
|
opts = append(opts, WithPhone())
|
|
}
|
|
if passwordWM {
|
|
opts = append(opts, WithPassword())
|
|
}
|
|
if avatarWM {
|
|
opts = append(opts, WithAvatar())
|
|
}
|
|
if idpLinks {
|
|
opts = append(opts, WithIDPLinks())
|
|
}
|
|
return newUserV2WriteModel(userID, resourceOwner, opts...)
|
|
}
|
|
|
|
func newUserV2WriteModel(userID, resourceOwner string, opts ...UserV2WMOption) *UserV2WriteModel {
|
|
wm := &UserV2WriteModel{
|
|
WriteModel: eventstore.WriteModel{
|
|
AggregateID: userID,
|
|
ResourceOwner: resourceOwner,
|
|
},
|
|
}
|
|
|
|
for _, optFunc := range opts {
|
|
optFunc(wm)
|
|
}
|
|
return wm
|
|
}
|
|
|
|
type UserV2WMOption func(o *UserV2WriteModel)
|
|
|
|
func WithHuman() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.HumanWriteModel = true
|
|
}
|
|
}
|
|
func WithMachine() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.MachineWriteModel = true
|
|
}
|
|
}
|
|
func WithProfile() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.ProfileWriteModel = true
|
|
}
|
|
}
|
|
func WithEmail() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.EmailWriteModel = true
|
|
}
|
|
}
|
|
func WithPhone() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.PhoneWriteModel = true
|
|
}
|
|
}
|
|
func WithPassword() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.PasswordWriteModel = true
|
|
}
|
|
}
|
|
func WithState() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.StateWriteModel = true
|
|
}
|
|
}
|
|
func WithAvatar() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.AvatarWriteModel = true
|
|
}
|
|
}
|
|
func WithIDPLinks() UserV2WMOption {
|
|
return func(o *UserV2WriteModel) {
|
|
o.IDPLinkWriteModel = true
|
|
}
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) Reduce() error {
|
|
for _, event := range wm.Events {
|
|
switch e := event.(type) {
|
|
case *user.HumanAddedEvent:
|
|
wm.reduceHumanAddedEvent(e)
|
|
case *user.HumanRegisteredEvent:
|
|
wm.reduceHumanRegisteredEvent(e)
|
|
|
|
case *user.HumanInitialCodeAddedEvent:
|
|
wm.UserState = domain.UserStateInitial
|
|
wm.SetInitCode(e.Code, e.Expiry, e.CreationDate())
|
|
case *user.HumanInitializedCheckSucceededEvent:
|
|
wm.UserState = domain.UserStateActive
|
|
wm.EmptyInitCode()
|
|
case *user.HumanInitializedCheckFailedEvent:
|
|
wm.InitCheckFailedCount += 1
|
|
|
|
case *user.UsernameChangedEvent:
|
|
wm.UserName = e.UserName
|
|
case *user.HumanProfileChangedEvent:
|
|
wm.reduceHumanProfileChangedEvent(e)
|
|
|
|
case *user.MachineChangedEvent:
|
|
if e.Name != nil {
|
|
wm.Name = *e.Name
|
|
}
|
|
if e.Description != nil {
|
|
wm.Description = *e.Description
|
|
}
|
|
if e.AccessTokenType != nil {
|
|
wm.AccessTokenType = *e.AccessTokenType
|
|
}
|
|
|
|
case *user.MachineAddedEvent:
|
|
wm.UserName = e.UserName
|
|
wm.Name = e.Name
|
|
wm.Description = e.Description
|
|
wm.AccessTokenType = e.AccessTokenType
|
|
wm.UserState = domain.UserStateActive
|
|
|
|
case *user.HumanEmailChangedEvent:
|
|
wm.Email = e.EmailAddress
|
|
wm.IsEmailVerified = false
|
|
wm.EmptyEmailCode()
|
|
case *user.HumanEmailCodeAddedEvent:
|
|
wm.IsEmailVerified = false
|
|
wm.SetEMailCode(e.Code, e.Expiry, e.CreationDate())
|
|
case *user.HumanEmailVerifiedEvent:
|
|
wm.IsEmailVerified = true
|
|
wm.EmptyEmailCode()
|
|
case *user.HumanEmailVerificationFailedEvent:
|
|
wm.EmailCheckFailedCount += 1
|
|
|
|
case *user.HumanPhoneChangedEvent:
|
|
wm.IsPhoneVerified = false
|
|
wm.Phone = e.PhoneNumber
|
|
wm.EmptyPhoneCode()
|
|
case *user.HumanPhoneCodeAddedEvent:
|
|
wm.IsPhoneVerified = false
|
|
wm.SetPhoneCode(e.Code, e.Expiry, e.CreationDate())
|
|
case *user.HumanPhoneVerifiedEvent:
|
|
wm.IsPhoneVerified = true
|
|
wm.EmptyPhoneCode()
|
|
case *user.HumanPhoneVerificationFailedEvent:
|
|
wm.PhoneCheckFailedCount += 1
|
|
case *user.HumanPhoneRemovedEvent:
|
|
wm.EmptyPhoneCode()
|
|
wm.Phone = ""
|
|
wm.IsPhoneVerified = false
|
|
|
|
case *user.HumanAvatarAddedEvent:
|
|
wm.Avatar = e.StoreKey
|
|
case *user.HumanAvatarRemovedEvent:
|
|
wm.Avatar = ""
|
|
|
|
case *user.UserLockedEvent:
|
|
wm.UserState = domain.UserStateLocked
|
|
case *user.UserUnlockedEvent:
|
|
wm.PasswordCheckFailedCount = 0
|
|
wm.UserState = domain.UserStateActive
|
|
|
|
case *user.UserDeactivatedEvent:
|
|
wm.UserState = domain.UserStateInactive
|
|
case *user.UserReactivatedEvent:
|
|
wm.UserState = domain.UserStateActive
|
|
|
|
case *user.UserRemovedEvent:
|
|
wm.UserState = domain.UserStateDeleted
|
|
|
|
case *user.HumanPasswordHashUpdatedEvent:
|
|
wm.PasswordEncodedHash = e.EncodedHash
|
|
case *user.HumanPasswordCheckFailedEvent:
|
|
wm.PasswordCheckFailedCount += 1
|
|
case *user.HumanPasswordCheckSucceededEvent:
|
|
wm.PasswordCheckFailedCount = 0
|
|
case *user.HumanPasswordChangedEvent:
|
|
wm.PasswordEncodedHash = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
|
wm.PasswordChangeRequired = e.ChangeRequired
|
|
wm.EmptyPasswordCode()
|
|
case *user.HumanPasswordCodeAddedEvent:
|
|
wm.SetPasswordCode(e)
|
|
case *user.HumanPasswordCodeSentEvent:
|
|
wm.SetPasswordCodeSent(e)
|
|
case *user.UserIDPLinkAddedEvent:
|
|
wm.AddIDPLink(e.IDPConfigID, e.DisplayName, e.ExternalUserID)
|
|
case *user.UserIDPLinkRemovedEvent:
|
|
wm.RemoveIDPLink(e.IDPConfigID, e.ExternalUserID)
|
|
case *user.UserIDPLinkCascadeRemovedEvent:
|
|
wm.RemoveIDPLink(e.IDPConfigID, e.ExternalUserID)
|
|
}
|
|
}
|
|
return wm.WriteModel.Reduce()
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) AddIDPLink(configID, displayName, externalUserID string) {
|
|
wm.IDPLinks = append(wm.IDPLinks, &domain.UserIDPLink{IDPConfigID: configID, DisplayName: displayName, ExternalUserID: externalUserID})
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) RemoveIDPLink(configID, externalUserID string) {
|
|
idx, _ := wm.IDPLinkByID(configID, externalUserID)
|
|
if idx < 0 {
|
|
return
|
|
}
|
|
copy(wm.IDPLinks[idx:], wm.IDPLinks[idx+1:])
|
|
wm.IDPLinks[len(wm.IDPLinks)-1] = nil
|
|
wm.IDPLinks = wm.IDPLinks[:len(wm.IDPLinks)-1]
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) EmptyInitCode() {
|
|
wm.InitCode = nil
|
|
wm.InitCodeExpiry = 0
|
|
wm.InitCodeCreationDate = time.Time{}
|
|
wm.InitCheckFailedCount = 0
|
|
}
|
|
func (wm *UserV2WriteModel) SetInitCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
|
|
wm.InitCode = code
|
|
wm.InitCodeExpiry = expiry
|
|
wm.InitCodeCreationDate = creationDate
|
|
wm.InitCheckFailedCount = 0
|
|
}
|
|
func (wm *UserV2WriteModel) EmptyEmailCode() {
|
|
wm.EmailCode = nil
|
|
wm.EmailCodeExpiry = 0
|
|
wm.EmailCodeCreationDate = time.Time{}
|
|
wm.EmailCheckFailedCount = 0
|
|
}
|
|
func (wm *UserV2WriteModel) SetEMailCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
|
|
wm.EmailCode = code
|
|
wm.EmailCodeExpiry = expiry
|
|
wm.EmailCodeCreationDate = creationDate
|
|
wm.EmailCheckFailedCount = 0
|
|
}
|
|
func (wm *UserV2WriteModel) EmptyPhoneCode() {
|
|
wm.PhoneCode = nil
|
|
wm.PhoneCodeExpiry = 0
|
|
wm.PhoneCodeCreationDate = time.Time{}
|
|
wm.PhoneCheckFailedCount = 0
|
|
}
|
|
func (wm *UserV2WriteModel) SetPhoneCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
|
|
wm.PhoneCode = code
|
|
wm.PhoneCodeExpiry = expiry
|
|
wm.PhoneCodeCreationDate = creationDate
|
|
wm.PhoneCheckFailedCount = 0
|
|
}
|
|
func (wm *UserV2WriteModel) EmptyPasswordCode() {
|
|
wm.PasswordCode = nil
|
|
wm.PasswordCodeExpiry = 0
|
|
wm.PasswordCodeCreationDate = time.Time{}
|
|
}
|
|
func (wm *UserV2WriteModel) SetPasswordCode(e *user.HumanPasswordCodeAddedEvent) {
|
|
wm.PasswordCode = e.Code
|
|
wm.PasswordCodeExpiry = e.Expiry
|
|
wm.PasswordCodeCreationDate = e.CreationDate()
|
|
wm.PasswordCodeGeneratorID = e.GeneratorID
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) SetPasswordCodeSent(e *user.HumanPasswordCodeSentEvent) {
|
|
wm.PasswordCodeGeneratorID = e.GeneratorInfo.GetID()
|
|
wm.PasswordCodeVerificationID = e.GeneratorInfo.GetVerificationID()
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) Query() *eventstore.SearchQueryBuilder {
|
|
// remove events are always processed
|
|
// and username is based for machine and human
|
|
eventTypes := []eventstore.EventType{
|
|
user.UserRemovedType,
|
|
user.UserUserNameChangedType,
|
|
}
|
|
|
|
if wm.HumanWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.UserV1AddedType,
|
|
user.HumanAddedType,
|
|
user.UserV1RegisteredType,
|
|
user.HumanRegisteredType,
|
|
)
|
|
}
|
|
|
|
if wm.MachineWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.MachineChangedEventType,
|
|
user.MachineAddedEventType,
|
|
)
|
|
}
|
|
|
|
if wm.EmailWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.UserV1EmailChangedType,
|
|
user.HumanEmailChangedType,
|
|
user.UserV1EmailCodeAddedType,
|
|
user.HumanEmailCodeAddedType,
|
|
|
|
user.UserV1EmailVerifiedType,
|
|
user.HumanEmailVerifiedType,
|
|
user.HumanEmailVerificationFailedType,
|
|
user.UserV1EmailVerificationFailedType,
|
|
)
|
|
}
|
|
if wm.PhoneWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.UserV1PhoneChangedType,
|
|
user.HumanPhoneChangedType,
|
|
user.UserV1PhoneCodeAddedType,
|
|
user.HumanPhoneCodeAddedType,
|
|
|
|
user.UserV1PhoneVerifiedType,
|
|
user.HumanPhoneVerifiedType,
|
|
user.HumanPhoneVerificationFailedType,
|
|
user.UserV1PhoneVerificationFailedType,
|
|
|
|
user.UserV1PhoneRemovedType,
|
|
user.HumanPhoneRemovedType,
|
|
)
|
|
}
|
|
if wm.ProfileWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.UserV1ProfileChangedType,
|
|
user.HumanProfileChangedType,
|
|
)
|
|
}
|
|
if wm.StateWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.UserV1InitialCodeAddedType,
|
|
user.HumanInitialCodeAddedType,
|
|
|
|
user.UserV1InitializedCheckSucceededType,
|
|
user.HumanInitializedCheckSucceededType,
|
|
user.HumanInitializedCheckFailedType,
|
|
user.UserV1InitializedCheckFailedType,
|
|
|
|
user.UserLockedType,
|
|
user.UserUnlockedType,
|
|
user.UserDeactivatedType,
|
|
user.UserReactivatedType,
|
|
)
|
|
}
|
|
if wm.AvatarWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.HumanAvatarAddedType,
|
|
user.HumanAvatarRemovedType,
|
|
)
|
|
}
|
|
if wm.PasswordWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.HumanPasswordHashUpdatedType,
|
|
|
|
user.HumanPasswordChangedType,
|
|
user.UserV1PasswordChangedType,
|
|
user.HumanPasswordCodeAddedType,
|
|
user.UserV1PasswordCodeAddedType,
|
|
|
|
user.HumanPasswordCheckFailedType,
|
|
user.UserV1PasswordCheckFailedType,
|
|
user.HumanPasswordCheckSucceededType,
|
|
user.UserV1PasswordCheckSucceededType,
|
|
)
|
|
}
|
|
if wm.IDPLinkWriteModel {
|
|
eventTypes = append(eventTypes,
|
|
user.UserIDPLinkAddedType,
|
|
user.UserIDPLinkRemovedType,
|
|
user.UserIDPLinkCascadeRemovedType,
|
|
)
|
|
}
|
|
|
|
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
|
AddQuery().
|
|
AggregateTypes(user.AggregateType).
|
|
AggregateIDs(wm.AggregateID).
|
|
EventTypes(eventTypes...).
|
|
Builder()
|
|
if wm.ResourceOwner != "" {
|
|
query.ResourceOwner(wm.ResourceOwner)
|
|
}
|
|
return query
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) reduceHumanAddedEvent(e *user.HumanAddedEvent) {
|
|
wm.UserName = e.UserName
|
|
wm.FirstName = e.FirstName
|
|
wm.LastName = e.LastName
|
|
wm.NickName = e.NickName
|
|
wm.DisplayName = e.DisplayName
|
|
wm.PreferredLanguage = e.PreferredLanguage
|
|
wm.Gender = e.Gender
|
|
wm.Email = e.EmailAddress
|
|
wm.Phone = e.PhoneNumber
|
|
wm.UserState = domain.UserStateActive
|
|
wm.PasswordEncodedHash = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
|
wm.PasswordChangeRequired = e.ChangeRequired
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) reduceHumanRegisteredEvent(e *user.HumanRegisteredEvent) {
|
|
wm.UserName = e.UserName
|
|
wm.FirstName = e.FirstName
|
|
wm.LastName = e.LastName
|
|
wm.NickName = e.NickName
|
|
wm.DisplayName = e.DisplayName
|
|
wm.PreferredLanguage = e.PreferredLanguage
|
|
wm.Gender = e.Gender
|
|
wm.Email = e.EmailAddress
|
|
wm.Phone = e.PhoneNumber
|
|
wm.UserState = domain.UserStateActive
|
|
wm.PasswordEncodedHash = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
|
wm.PasswordChangeRequired = e.ChangeRequired
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) reduceHumanProfileChangedEvent(e *user.HumanProfileChangedEvent) {
|
|
if e.FirstName != "" {
|
|
wm.FirstName = e.FirstName
|
|
}
|
|
if e.LastName != "" {
|
|
wm.LastName = e.LastName
|
|
}
|
|
if e.NickName != nil {
|
|
wm.NickName = *e.NickName
|
|
}
|
|
if e.DisplayName != nil {
|
|
wm.DisplayName = *e.DisplayName
|
|
}
|
|
if e.PreferredLanguage != nil {
|
|
wm.PreferredLanguage = *e.PreferredLanguage
|
|
}
|
|
if e.Gender != nil {
|
|
wm.Gender = *e.Gender
|
|
}
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) Aggregate() *user.Aggregate {
|
|
return user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) NewProfileChangedEvent(
|
|
ctx context.Context,
|
|
firstName,
|
|
lastName,
|
|
nickName,
|
|
displayName *string,
|
|
preferredLanguage *language.Tag,
|
|
gender *domain.Gender,
|
|
) (*user.HumanProfileChangedEvent, error) {
|
|
changes := make([]user.ProfileChanges, 0)
|
|
if firstName != nil && wm.FirstName != *firstName {
|
|
changes = append(changes, user.ChangeFirstName(*firstName))
|
|
}
|
|
if lastName != nil && wm.LastName != *lastName {
|
|
changes = append(changes, user.ChangeLastName(*lastName))
|
|
}
|
|
if nickName != nil && wm.NickName != *nickName {
|
|
changes = append(changes, user.ChangeNickName(*nickName))
|
|
}
|
|
if displayName != nil && wm.DisplayName != *displayName {
|
|
changes = append(changes, user.ChangeDisplayName(*displayName))
|
|
}
|
|
if preferredLanguage != nil && wm.PreferredLanguage != *preferredLanguage {
|
|
changes = append(changes, user.ChangePreferredLanguage(*preferredLanguage))
|
|
}
|
|
if gender != nil && wm.Gender != *gender {
|
|
changes = append(changes, user.ChangeGender(*gender))
|
|
}
|
|
if len(changes) == 0 {
|
|
return nil, nil
|
|
}
|
|
return user.NewHumanProfileChangedEvent(ctx, &wm.Aggregate().Aggregate, changes)
|
|
}
|
|
|
|
func (wm *UserV2WriteModel) IDPLinkByID(idpID, externalUserID string) (idx int, idp *domain.UserIDPLink) {
|
|
for idx, idp = range wm.IDPLinks {
|
|
if idp.IDPConfigID == idpID && idp.ExternalUserID == externalUserID {
|
|
return idx, idp
|
|
}
|
|
}
|
|
return -1, nil
|
|
}
|