mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
feat: user commands (#75)
* feat: eventstore repository * fix: remove gorm * version * feat: pkg * feat: add some files for project * feat: eventstore without eventstore-lib * rename files * gnueg * fix: key json * fix: add object * fix: change imports * fix: internal models * fix: some imports * fix: global model * feat: add global view functions * fix: add some functions on repo * feat(eventstore): sdk * fix(eventstore): search query * fix(eventstore): rename app to eventstore * delete empty test * remove unused func * merge master * fix(eventstore): tests * fix(models): delete unused struct * fix: some funcitons * feat(eventstore): implemented push events * fix: move project eventstore to project package * fix: change project eventstore funcs * feat(eventstore): overwrite context data * fix: change project eventstore * fix: add project repo to mgmt server * feat(types): SQL-config * fix: commented code * feat(eventstore): options to overwrite editor * feat: auth interceptor and cockroach migrations * fix: migrations * fix: fix filter * fix: not found on getbyid * fix: use global sql config * fix: add sequence * fix: add some tests * fix(eventstore): nullable sequence * fix: add some tests * merge * fix: add some tests * fix(migrations): correct statements for sequence * fix: add some tests * fix: add some tests * fix: changes from mr * fix: changes from mr * fix: add some tests * Update internal/eventstore/models/field.go Co-Authored-By: livio-a <livio.a@gmail.com> * fix(eventstore): code quality * fix: add types to aggregate/Event-types * fix: try tests * fix(eventstore): rename modifier* to editor* * fix(eventstore): delete editor_org * fix(migrations): remove editor_org field, rename modifier_* to editor_* * fix: query tests * fix: use prepare funcs * fix: go mod * fix: generate files * fix(eventstore): tests * fix(eventstore): rename modifier to editor * fix(migrations): add cluster migration, fix(migrations): fix typo of host in clean clsuter * fix(eventstore): move health * fix(eventstore): AggregateTypeFilter aggregateType as param * code quality * fix: go tests * feat: add member funcs * feat: add member model * feat: add member events * feat: add member repo model * fix: better error func testing * fix: project member funcs * fix: add tests * fix: add tests * feat: implement member requests * fix: merge master * fix: merge master * fix: read existing in project repo * fix: fix tests * feat: add internal cache * feat: add cache mock * fix: return values of cache mock * feat: add project role * fix: add cache config * fix: add role to eventstore * fix: use eventstore sdk * fix: use eventstore sdk * fix: add project role grpc requests * fix: fix getby id * fix: changes for mr * fix: change value to interface * feat: add app event creations * fix: searchmethods * Update internal/project/model/project_member.go Co-Authored-By: Silvan <silvan.reusser@gmail.com> * fix: use get project func * fix: append events * fix: check if value is string on equal ignore case * fix: add changes test * fix: add go mod * fix: add some tests * fix: return err not nil * fix: return err not nil * fix: add aggregate funcs and tests * fix: add oidc aggregate funcs and tests * fix: add oidc * fix: add some tests * fix: tests * fix: oidc validation * fix: generate client secret * fix: generate client id * fix: test change app * fix: deactivate/reactivate application * fix: change oidc config * fix: change oidc config secret * fix: implement grpc app funcs * fix: add application requests * fix: converter * fix: converter * fix: converter and generate clientid * fix: tests * feat: project grant aggregate * feat: project grant * fix: project grant check if role existing * fix: project grant requests * fix: project grant fixes * fix: project grant member model * fix: project grant member aggregate * fix: project grant member eventstore * fix: project grant member requests * feat: user model * feat: user command side * user command side * profile requests * local config with gopass and more * Update internal/user/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/address.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/address.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/mfa.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/mfa.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/model/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/model/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/user_test.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/eventstore_mock_test.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * changes from mr review * save files into basedir * changes from mr review * changes from mr review * Update internal/usergrant/repository/eventsourcing/cache.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/usergrant/repository/eventsourcing/cache.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * changes requested on mr * fix generate codes * fix return if no events * password code * Update internal/user/repository/eventsourcing/model/password.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * requests of mr * check email Co-authored-by: adlerhurst <silvan.reusser@gmail.com> Co-authored-by: livio-a <livio.a@gmail.com>
This commit is contained in:
13
internal/user/model/address.go
Normal file
13
internal/user/model/address.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
import es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
|
||||
type Address struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Country string
|
||||
Locality string
|
||||
PostalCode string
|
||||
Region string
|
||||
StreetAddress string
|
||||
}
|
44
internal/user/model/email.go
Normal file
44
internal/user/model/email.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
EmailAddress string
|
||||
IsEmailVerified bool
|
||||
}
|
||||
|
||||
type EmailCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
func (e *Email) IsValid() bool {
|
||||
return e.EmailAddress != ""
|
||||
}
|
||||
|
||||
func (e *Email) GenerateEmailCodeIfNeeded(emailGenerator crypto.Generator) (*EmailCode, error) {
|
||||
var emailCode *EmailCode
|
||||
if e.IsEmailVerified {
|
||||
return emailCode, nil
|
||||
}
|
||||
emailCode = new(EmailCode)
|
||||
return emailCode, emailCode.GenerateEmailCode(emailGenerator)
|
||||
}
|
||||
|
||||
func (code *EmailCode) GenerateEmailCode(emailGenerator crypto.Generator) error {
|
||||
emailCodeCrypto, _, err := crypto.NewCode(emailGenerator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
code.Code = emailCodeCrypto
|
||||
code.Expiry = emailGenerator.Expiry()
|
||||
return nil
|
||||
}
|
23
internal/user/model/mfa.go
Normal file
23
internal/user/model/mfa.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
)
|
||||
|
||||
type OTP struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Secret *crypto.CryptoValue
|
||||
SecretString string
|
||||
Url string
|
||||
State MfaState
|
||||
}
|
||||
|
||||
type MfaState int32
|
||||
|
||||
const (
|
||||
MFASTATE_UNSPECIFIED MfaState = iota
|
||||
MFASTATE_NOTREADY
|
||||
MFASTATE_READY
|
||||
)
|
47
internal/user/model/password.go
Normal file
47
internal/user/model/password.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Password struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
SecretString string
|
||||
SecretCrypto *crypto.CryptoValue
|
||||
ChangeRequired bool
|
||||
}
|
||||
|
||||
type PasswordCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
Expiry time.Duration
|
||||
NotificationType NotificationType
|
||||
}
|
||||
|
||||
type NotificationType int32
|
||||
|
||||
const (
|
||||
NOTIFICATIONTYPE_EMAIL NotificationType = iota
|
||||
NOTIFICATIONTYPE_SMS
|
||||
)
|
||||
|
||||
func (p *Password) IsValid() bool {
|
||||
return p.AggregateID != "" && p.SecretString != ""
|
||||
}
|
||||
|
||||
func (p *Password) HashPasswordIfExisting(passwordAlg crypto.HashAlgorithm, onetime bool) error {
|
||||
if p.SecretString == "" {
|
||||
return nil
|
||||
}
|
||||
secret, err := crypto.Hash([]byte(p.SecretString), passwordAlg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.SecretCrypto = secret
|
||||
p.ChangeRequired = onetime
|
||||
return nil
|
||||
}
|
67
internal/user/model/phone.go
Normal file
67
internal/user/model/phone.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Phone struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
PhoneNumber string
|
||||
IsPhoneVerified bool
|
||||
}
|
||||
|
||||
type PhoneCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
func (p *Phone) IsValid() bool {
|
||||
return p.PhoneNumber != ""
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneChangedEvent(event *es_models.Event) error {
|
||||
u.Phone = new(Phone)
|
||||
u.Phone.setData(event)
|
||||
u.IsPhoneVerified = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneVerifiedEvent() error {
|
||||
u.IsPhoneVerified = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Phone) setData(event *es_models.Event) error {
|
||||
p.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, p); err != nil {
|
||||
logging.Log("EVEN-dlo9s").WithError(err).Error("could not unmarshal event data")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Phone) GeneratePhoneCodeIfNeeded(phoneGenerator crypto.Generator) (*PhoneCode, error) {
|
||||
var phoneCode *PhoneCode
|
||||
if p.IsPhoneVerified {
|
||||
return phoneCode, nil
|
||||
}
|
||||
phoneCode = new(PhoneCode)
|
||||
return phoneCode, phoneCode.GeneratePhoneCode(phoneGenerator)
|
||||
}
|
||||
|
||||
func (code *PhoneCode) GeneratePhoneCode(phoneGenerator crypto.Generator) error {
|
||||
phoneCodeCrypto, _, err := crypto.NewCode(phoneGenerator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
code.Code = phoneCodeCrypto
|
||||
code.Expiry = phoneGenerator.Expiry()
|
||||
return nil
|
||||
}
|
22
internal/user/model/profile.go
Normal file
22
internal/user/model/profile.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
UserName string
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
PreferredLanguage language.Tag
|
||||
Gender Gender
|
||||
}
|
||||
|
||||
func (p *Profile) IsValid() bool {
|
||||
return p.FirstName != "" && p.LastName != ""
|
||||
}
|
126
internal/user/model/user.go
Normal file
126
internal/user/model/user.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
State UserState
|
||||
*Password
|
||||
*Profile
|
||||
*Email
|
||||
*Phone
|
||||
*Address
|
||||
InitCode *InitUserCode
|
||||
EmailCode *EmailCode
|
||||
PhoneCode *PhoneCode
|
||||
PasswordCode *PasswordCode
|
||||
OTP *OTP
|
||||
}
|
||||
|
||||
type InitUserCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
type UserState int32
|
||||
|
||||
const (
|
||||
USERSTATE_UNSPECIFIED UserState = iota
|
||||
USERSTATE_ACTIVE
|
||||
USERSTATE_INACTIVE
|
||||
USERSTATE_DELETED
|
||||
USERSTATE_LOCKED
|
||||
USERSTATE_SUSPEND
|
||||
USERSTATE_INITIAL
|
||||
)
|
||||
|
||||
type Gender int32
|
||||
|
||||
const (
|
||||
GENDER_UNDEFINED Gender = iota
|
||||
GENDER_FEMALE
|
||||
GENDER_MALE
|
||||
GENDER_DIVERSE
|
||||
)
|
||||
|
||||
func (u *User) SetEmailAsUsername() {
|
||||
if u.Profile != nil && u.UserName == "" && u.Email != nil {
|
||||
u.UserName = u.EmailAddress
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) IsValid() bool {
|
||||
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.UserName != "" && u.Email != nil && u.EmailAddress != ""
|
||||
}
|
||||
|
||||
func (u *User) IsInitialState() bool {
|
||||
return u.Email == nil || !u.IsEmailVerified || u.Password == nil || u.SecretString == ""
|
||||
}
|
||||
|
||||
func (u *User) IsActive() bool {
|
||||
return u.State == USERSTATE_ACTIVE
|
||||
}
|
||||
|
||||
func (u *User) IsInitial() bool {
|
||||
return u.State == USERSTATE_INITIAL
|
||||
}
|
||||
|
||||
func (u *User) IsInactive() bool {
|
||||
return u.State == USERSTATE_INACTIVE
|
||||
}
|
||||
|
||||
func (u *User) IsLocked() bool {
|
||||
return u.State == USERSTATE_LOCKED
|
||||
}
|
||||
|
||||
func (u *User) IsOTPReady() bool {
|
||||
return u.OTP != nil && u.OTP.State == MFASTATE_READY
|
||||
}
|
||||
|
||||
func (u *User) HashPasswordIfExisting(passwordAlg crypto.HashAlgorithm, onetime bool) error {
|
||||
if u.Password != nil {
|
||||
return u.Password.HashPasswordIfExisting(passwordAlg, onetime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) GenerateInitCodeIfNeeded(initGenerator crypto.Generator) error {
|
||||
u.InitCode = new(InitUserCode)
|
||||
if !u.IsInitialState() {
|
||||
return nil
|
||||
}
|
||||
return u.InitCode.GenerateInitUserCode(initGenerator)
|
||||
}
|
||||
|
||||
func (u *User) GeneratePhoneCodeIfNeeded(phoneGenerator crypto.Generator) error {
|
||||
u.PhoneCode = new(PhoneCode)
|
||||
if u.Phone == nil || u.IsPhoneVerified {
|
||||
return nil
|
||||
}
|
||||
return u.PhoneCode.GeneratePhoneCode(phoneGenerator)
|
||||
}
|
||||
|
||||
func (u *User) GenerateEmailCodeIfNeeded(emailGenerator crypto.Generator) error {
|
||||
u.EmailCode = new(EmailCode)
|
||||
if u.Email == nil || u.IsEmailVerified {
|
||||
return nil
|
||||
}
|
||||
return u.EmailCode.GenerateEmailCode(emailGenerator)
|
||||
}
|
||||
|
||||
func (init *InitUserCode) GenerateInitUserCode(generator crypto.Generator) error {
|
||||
initCodeCrypto, _, err := crypto.NewCode(generator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
init.Code = initCodeCrypto
|
||||
init.Expiry = generator.Expiry()
|
||||
return nil
|
||||
}
|
35
internal/user/repository/eventsourcing/cache.go
Normal file
35
internal/user/repository/eventsourcing/cache.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/cache"
|
||||
"github.com/caos/zitadel/internal/cache/config"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
type UserCache struct {
|
||||
userCache cache.Cache
|
||||
}
|
||||
|
||||
func StartCache(conf *config.CacheConfig) (*UserCache, error) {
|
||||
userCache, err := conf.Config.NewCache()
|
||||
logging.Log("EVENT-vDneN").OnError(err).Panic("unable to create user cache")
|
||||
|
||||
return &UserCache{userCache: userCache}, nil
|
||||
}
|
||||
|
||||
func (c *UserCache) getUser(ID string) *model.User {
|
||||
user := &model.User{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
|
||||
if err := c.userCache.Get(ID, user); err != nil {
|
||||
logging.Log("EVENT-4eTZh").WithError(err).Debug("error in getting cache")
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (c *UserCache) cacheUser(user *model.User) {
|
||||
err := c.userCache.Set(user.AggregateID, user)
|
||||
if err != nil {
|
||||
logging.Log("EVENT-ThnBb").WithError(err).Debug("error in setting project cache")
|
||||
}
|
||||
}
|
18
internal/user/repository/eventsourcing/codes.go
Normal file
18
internal/user/repository/eventsourcing/codes.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
func (es *UserEventstore) generatePasswordCode(passwordCode *model.PasswordCode, notifyType usr_model.NotificationType) error {
|
||||
passwordCodeCrypto, _, err := crypto.NewCode(es.PasswordVerificationCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passwordCode.Code = passwordCodeCrypto
|
||||
passwordCode.Expiry = es.PasswordVerificationCode.Expiry()
|
||||
passwordCode.NotificationType = int32(notifyType)
|
||||
return nil
|
||||
}
|
729
internal/user/repository/eventsourcing/eventstore.go
Normal file
729
internal/user/repository/eventsourcing/eventstore.go
Normal file
@@ -0,0 +1,729 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/cache/config"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_int "github.com/caos/zitadel/internal/eventstore"
|
||||
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/sony/sonyflake"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type UserEventstore struct {
|
||||
es_int.Eventstore
|
||||
userCache *UserCache
|
||||
idGenerator *sonyflake.Sonyflake
|
||||
PasswordAlg crypto.HashAlgorithm
|
||||
InitializeUserCode crypto.Generator
|
||||
EmailVerificationCode crypto.Generator
|
||||
PhoneVerificationCode crypto.Generator
|
||||
PasswordVerificationCode crypto.Generator
|
||||
Multifactors global_model.Multifactors
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
es_int.Eventstore
|
||||
Cache *config.CacheConfig
|
||||
PasswordSaltCost int
|
||||
}
|
||||
|
||||
func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventstore, error) {
|
||||
userCache, err := StartCache(conf.Cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idGenerator := sonyflake.NewSonyflake(sonyflake.Settings{})
|
||||
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.UserVerificationKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
initCodeGen := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.InitializeUserCode, aesCrypto)
|
||||
emailVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.EmailVerificationCode, aesCrypto)
|
||||
phoneVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PhoneVerificationCode, aesCrypto)
|
||||
passwordVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PasswordVerificationCode, aesCrypto)
|
||||
aesOtpCrypto, err := crypto.NewAESCrypto(systemDefaults.Multifactors.OTP.VerificationKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mfa := global_model.Multifactors{
|
||||
OTP: global_model.OTP{
|
||||
CryptoMFA: aesOtpCrypto,
|
||||
Issuer: systemDefaults.Multifactors.OTP.Issuer,
|
||||
},
|
||||
}
|
||||
return &UserEventstore{
|
||||
Eventstore: conf.Eventstore,
|
||||
userCache: userCache,
|
||||
idGenerator: idGenerator,
|
||||
InitializeUserCode: initCodeGen,
|
||||
EmailVerificationCode: emailVerificationCode,
|
||||
PhoneVerificationCode: phoneVerificationCode,
|
||||
PasswordVerificationCode: passwordVerificationCode,
|
||||
Multifactors: mfa,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.User, error) {
|
||||
user := es.userCache.getUser(id)
|
||||
|
||||
query, err := UserByIDQuery(user.AggregateID, user.Sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = es_sdk.Filter(ctx, es.FilterEvents, user.AppendEvents, query)
|
||||
if err != nil && caos_errs.IsNotFound(err) && user.Sequence == 0 {
|
||||
return nil, err
|
||||
}
|
||||
es.userCache.cacheUser(user)
|
||||
return model.UserToModel(user), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) {
|
||||
user.SetEmailAsUsername()
|
||||
if !user.IsValid() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "Name is required")
|
||||
}
|
||||
//TODO: Check Uniqueness
|
||||
id, err := es.idGenerator.NextID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.AggregateID = strconv.FormatUint(id, 10)
|
||||
|
||||
err = user.HashPasswordIfExisting(es.PasswordAlg, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = user.GenerateInitCodeIfNeeded(es.InitializeUserCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = user.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoInitCode := model.InitCodeFromModel(user.InitCode)
|
||||
repoPhoneCode := model.PhoneCodeFromModel(user.PhoneCode)
|
||||
|
||||
createAggregate := UserCreateAggregate(es.AggregateCreator(), repoUser, repoInitCode, repoPhoneCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, createAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return model.UserToModel(repoUser), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) {
|
||||
user.SetEmailAsUsername()
|
||||
if !user.IsValid() || user.Password == nil || user.SecretString == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "user is invalid")
|
||||
}
|
||||
//TODO: Check Uniqueness
|
||||
id, err := es.idGenerator.NextID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.AggregateID = strconv.FormatUint(id, 10)
|
||||
|
||||
err = user.HashPasswordIfExisting(es.PasswordAlg, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = user.GenerateEmailCodeIfNeeded(es.EmailVerificationCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoEmailCode := model.EmailCodeFromModel(user.EmailCode)
|
||||
|
||||
createAggregate := UserRegisterAggregate(es.AggregateCreator(), repoUser, resourceOwner, repoEmailCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, createAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return model.UserToModel(repoUser), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) DeactivateUser(ctx context.Context, id string) (*usr_model.User, error) {
|
||||
existing, err := es.UserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.IsInactive() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-die45", "cant deactivate inactive user")
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
aggregate := UserDeactivateAggregate(es.AggregateCreator(), repoExisting)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return model.UserToModel(repoExisting), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ReactivateUser(ctx context.Context, id string) (*usr_model.User, error) {
|
||||
existing, err := es.UserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !existing.IsInactive() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do94s", "user must be inactive")
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
aggregate := UserReactivateAggregate(es.AggregateCreator(), repoExisting)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return model.UserToModel(repoExisting), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) LockUser(ctx context.Context, id string) (*usr_model.User, error) {
|
||||
existing, err := es.UserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !existing.IsActive() && !existing.IsInitial() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di83s", "user must be active or initial")
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
aggregate := UserLockAggregate(es.AggregateCreator(), repoExisting)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return model.UserToModel(repoExisting), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) UnlockUser(ctx context.Context, id string) (*usr_model.User, error) {
|
||||
existing, err := es.UserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !existing.IsLocked() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dks83", "user must be locked")
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
aggregate := UserUnlockAggregate(es.AggregateCreator(), repoExisting)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return model.UserToModel(repoExisting), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) InitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d8diw", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.InitCode != nil {
|
||||
return user.InitCode, nil
|
||||
}
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-d8e2", "init code not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CreateInitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
initCode := new(usr_model.InitUserCode)
|
||||
err = initCode.GenerateInitUserCode(es.InitializeUserCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoInitCode := model.InitCodeFromModel(initCode)
|
||||
|
||||
agg := UserInitCodeAggregate(es.AggregateCreator(), repoUser, repoInitCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return model.InitCodeToModel(repoUser.InitCode), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SkipMfaInit(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := SkipMfaAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) UserPasswordByID(ctx context.Context, userID string) (*usr_model.Password, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.Password != nil {
|
||||
return user.Password, nil
|
||||
}
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-d8e2", "password not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
|
||||
return es.changedPassword(ctx, password, true)
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SetPassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
|
||||
return es.changedPassword(ctx, password, false)
|
||||
}
|
||||
|
||||
func (es *UserEventstore) changedPassword(ctx context.Context, password *usr_model.Password, onetime bool) (*usr_model.Password, error) {
|
||||
if !password.IsValid() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dosi3", "password invalid")
|
||||
}
|
||||
user, err := es.UserByID(ctx, password.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = password.HashPasswordIfExisting(es.PasswordAlg, onetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoPassword := model.PasswordFromModel(password)
|
||||
|
||||
agg := PasswordChangeAggregate(es.AggregateCreator(), repoUser, repoPassword)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
|
||||
return model.PasswordToModel(repoUser.Password), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string, notifyType usr_model.NotificationType) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passwordCode := new(model.PasswordCode)
|
||||
err = es.generatePasswordCode(passwordCode, notifyType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := RequestSetPassword(es.AggregateCreator(), repoUser, passwordCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.Profile != nil {
|
||||
return user.Profile, nil
|
||||
}
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dk23f", "profile not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ChangeProfile(ctx context.Context, profile *usr_model.Profile) (*usr_model.Profile, error) {
|
||||
if !profile.IsValid() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d82i3", "profile is invalid")
|
||||
}
|
||||
existing, err := es.UserByID(ctx, profile.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
repoNew := model.ProfileFromModel(profile)
|
||||
|
||||
updateAggregate := ProfileChangeAggregate(es.AggregateCreator(), repoExisting, repoNew)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return model.ProfileToModel(repoExisting.Profile), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.Email != nil {
|
||||
return user.Email, nil
|
||||
}
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dki89", "email not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ChangeEmail(ctx context.Context, email *usr_model.Email) (*usr_model.Email, error) {
|
||||
if !email.IsValid() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "email is invalid")
|
||||
}
|
||||
existing, err := es.UserByID(ctx, email.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emailCode, err := email.GenerateEmailCodeIfNeeded(es.EmailVerificationCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
repoNew := model.EmailFromModel(email)
|
||||
repoEmailCode := model.EmailCodeFromModel(emailCode)
|
||||
|
||||
updateAggregate := EmailChangeAggregate(es.AggregateCreator(), repoExisting, repoNew, repoEmailCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return model.EmailToModel(repoExisting.Email), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) VerifyEmail(ctx context.Context, userID, verificationCode string) error {
|
||||
if userID == "" || verificationCode == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "userId or Code empty")
|
||||
}
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.EmailCode == nil {
|
||||
return caos_errs.ThrowNotFound(nil, "EVENT-lso9w", "code not found")
|
||||
}
|
||||
if err := crypto.VerifyCode(existing.EmailCode.CreationDate, existing.EmailCode.Expiry, existing.EmailCode.Code, verificationCode, es.EmailVerificationCode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
updateAggregate := EmailVerifiedAggregate(es.AggregateCreator(), repoExisting)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "userID missing")
|
||||
}
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.Email == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "no email existing")
|
||||
}
|
||||
if existing.IsEmailVerified {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "email already verified")
|
||||
}
|
||||
|
||||
emailCode := new(usr_model.EmailCode)
|
||||
err = emailCode.GenerateEmailCode(es.EmailVerificationCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
repoEmailCode := model.EmailCodeFromModel(emailCode)
|
||||
updateAggregate := EmailVerificationCodeAggregate(es.AggregateCreator(), repoExisting, repoEmailCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.Phone != nil {
|
||||
return user.Phone, nil
|
||||
}
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-pos9e", "phone not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ChangePhone(ctx context.Context, phone *usr_model.Phone) (*usr_model.Phone, error) {
|
||||
if !phone.IsValid() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9s4", "phone is invalid")
|
||||
}
|
||||
existing, err := es.UserByID(ctx, phone.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
phoneCode, err := phone.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
repoNew := model.PhoneFromModel(phone)
|
||||
repoPhoneCode := model.PhoneCodeFromModel(phoneCode)
|
||||
|
||||
updateAggregate := PhoneChangeAggregate(es.AggregateCreator(), repoExisting, repoNew, repoPhoneCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return model.PhoneToModel(repoExisting.Phone), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) VerifyPhone(ctx context.Context, userID, verificationCode string) error {
|
||||
if userID == "" || verificationCode == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dsi8s", "userId or Code empty")
|
||||
}
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.PhoneCode == nil {
|
||||
return caos_errs.ThrowNotFound(nil, "EVENT-slp0s", "code not found")
|
||||
}
|
||||
if err := crypto.VerifyCode(existing.PhoneCode.CreationDate, existing.PhoneCode.Expiry, existing.PhoneCode.Code, verificationCode, es.PhoneVerificationCode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
updateAggregate := PhoneVerifiedAggregate(es.AggregateCreator(), repoExisting)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CreatePhoneVerificationCode(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9sw", "userID missing")
|
||||
}
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.Phone == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp9fs", "no phone existing")
|
||||
}
|
||||
if existing.IsPhoneVerified {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sleis", "phone already verified")
|
||||
}
|
||||
|
||||
phoneCode := new(usr_model.PhoneCode)
|
||||
err = phoneCode.GeneratePhoneCode(es.PhoneVerificationCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
repoPhoneCode := model.PhoneCodeFromModel(phoneCode)
|
||||
updateAggregate := PhoneVerificationCodeAggregate(es.AggregateCreator(), repoExisting, repoPhoneCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di8ws", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.Address != nil {
|
||||
return user.Address, nil
|
||||
}
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-so9wa", "address not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ChangeAddress(ctx context.Context, address *usr_model.Address) (*usr_model.Address, error) {
|
||||
existing, err := es.UserByID(ctx, address.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
repoNew := model.AddressFromModel(address)
|
||||
|
||||
updateAggregate := AddressChangeAggregate(es.AggregateCreator(), repoExisting, repoNew)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return model.AddressToModel(repoExisting.Address), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) OTPByID(ctx context.Context, userID string) (*usr_model.OTP, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.OTP != nil {
|
||||
return user.OTP, nil
|
||||
}
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dps09", "otp not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) AddOTP(ctx context.Context, userID string) (*usr_model.OTP, error) {
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.IsOTPReady() {
|
||||
return nil, caos_errs.ThrowAlreadyExists(nil, "EVENT-do9se", "user has already configured otp")
|
||||
}
|
||||
key, err := totp.Generate(totp.GenerateOpts{Issuer: es.Multifactors.OTP.Issuer, AccountName: userID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedSecret, err := crypto.Encrypt([]byte(key.Secret()), es.Multifactors.OTP.CryptoMFA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoOtp := &model.OTP{Secret: encryptedSecret}
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
updateAggregate := MfaOTPAddAggregate(es.AggregateCreator(), repoExisting, repoOtp)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
otp := model.OTPToModel(repoExisting.OTP)
|
||||
otp.Url = key.URL()
|
||||
otp.SecretString = key.Secret()
|
||||
return otp, nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error {
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.OTP == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "no otp existing")
|
||||
}
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
updateAggregate := MfaOTPRemoveAggregate(es.AggregateCreator(), repoExisting)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoExisting)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string) error {
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.OTP == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "no otp existing")
|
||||
}
|
||||
decrypt, err := crypto.DecryptString(existing.OTP.Secret, es.Multifactors.OTP.CryptoMFA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valid := totp.Validate(code, decrypt)
|
||||
if !valid {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Invalid code")
|
||||
}
|
||||
return nil
|
||||
}
|
429
internal/user/repository/eventsourcing/eventstore_mock_test.go
Normal file
429
internal/user/repository/eventsourcing/eventstore_mock_test.go
Normal file
@@ -0,0 +1,429 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
mock_cache "github.com/caos/zitadel/internal/cache/mock"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore/mock"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sony/sonyflake"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetMockedEventstore(ctrl *gomock.Controller, mockEs *mock.MockEventstore) *UserEventstore {
|
||||
return &UserEventstore{
|
||||
Eventstore: mockEs,
|
||||
userCache: GetMockCache(ctrl),
|
||||
idGenerator: GetSonyFlacke(),
|
||||
}
|
||||
}
|
||||
|
||||
func GetMockedEventstoreWithPw(ctrl *gomock.Controller, mockEs *mock.MockEventstore, init, email, phone, password bool) *UserEventstore {
|
||||
es := &UserEventstore{
|
||||
Eventstore: mockEs,
|
||||
userCache: GetMockCache(ctrl),
|
||||
idGenerator: GetSonyFlacke(),
|
||||
}
|
||||
if init {
|
||||
es.InitializeUserCode = GetMockPwGenerator(ctrl)
|
||||
}
|
||||
if email {
|
||||
es.EmailVerificationCode = GetMockPwGenerator(ctrl)
|
||||
}
|
||||
if phone {
|
||||
es.PhoneVerificationCode = GetMockPwGenerator(ctrl)
|
||||
}
|
||||
if password {
|
||||
es.PasswordVerificationCode = GetMockPwGenerator(ctrl)
|
||||
hash := crypto.NewMockHashAlgorithm(ctrl)
|
||||
hash.EXPECT().Hash(gomock.Any()).Return(nil, nil)
|
||||
hash.EXPECT().Algorithm().Return("bcrypt")
|
||||
es.PasswordAlg = hash
|
||||
}
|
||||
return es
|
||||
}
|
||||
|
||||
func GetMockCache(ctrl *gomock.Controller) *UserCache {
|
||||
mockCache := mock_cache.NewMockCache(ctrl)
|
||||
mockCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
mockCache.EXPECT().Set(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
return &UserCache{userCache: mockCache}
|
||||
}
|
||||
|
||||
func GetSonyFlacke() *sonyflake.Sonyflake {
|
||||
return sonyflake.NewSonyflake(sonyflake.Settings{})
|
||||
}
|
||||
|
||||
func GetMockPwGenerator(ctrl *gomock.Controller) crypto.Generator {
|
||||
alg := crypto.CreateMockEncryptionAlg(ctrl)
|
||||
generator := crypto.NewMockGenerator(ctrl)
|
||||
generator.EXPECT().Length().Return(uint(10))
|
||||
generator.EXPECT().Runes().Return([]rune("abcdefghijklmnopqrstuvwxyz"))
|
||||
generator.EXPECT().Alg().AnyTimes().Return(alg)
|
||||
generator.EXPECT().Expiry().Return(time.Hour * 1)
|
||||
return generator
|
||||
}
|
||||
|
||||
func GetMockUserByIDOK(ctrl *gomock.Controller, user model.User) *UserEventstore {
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockUserByIDNoEvents(ctrl *gomock.Controller) *UserEventstore {
|
||||
events := []*es_models.Event{}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUser(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithPWGenerator(ctrl *gomock.Controller, init, email, phone, password bool) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
Email: &model.Email{
|
||||
EmailAddress: "EmailAddress",
|
||||
},
|
||||
Phone: &model.Phone{
|
||||
PhoneNumber: "PhoneNumber",
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstoreWithPw(ctrl, mockEs, init, email, phone, password)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithInitCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstoreWithPw(ctrl, mockEs, true, false, false, false)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithPasswordAndEmailCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstoreWithPw(ctrl, mockEs, false, true, false, true)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithEmailCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstoreWithPw(ctrl, mockEs, false, true, false, false)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithPhoneCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstoreWithPw(ctrl, mockEs, false, false, true, false)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithPasswordCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstoreWithPw(ctrl, mockEs, false, false, false, true)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithOTPGen(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
es := GetMockedEventstore(ctrl, mockEs)
|
||||
hash := crypto.NewMockEncryptionAlgorithm(ctrl)
|
||||
hash.EXPECT().Algorithm().Return("aes")
|
||||
hash.EXPECT().Encrypt(gomock.Any()).Return(nil, nil)
|
||||
hash.EXPECT().EncryptionKeyID().Return("id")
|
||||
es.Multifactors = global_model.Multifactors{OTP: global_model.OTP{
|
||||
Issuer: "Issuer",
|
||||
CryptoMFA: hash,
|
||||
}}
|
||||
return es
|
||||
}
|
||||
|
||||
func GetMockManipulateInactiveUser(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 2, Type: model.UserDeactivated},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateLockedUser(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserLocked},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithInitCode(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
}
|
||||
code := model.InitUserCode{Expiry: time.Hour * 30}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
dataCode, _ := json.Marshal(code)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.InitializedUserCodeAdded, Data: dataCode},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithEmailCode(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
Email: &model.Email{
|
||||
EmailAddress: "EmailAddress",
|
||||
},
|
||||
}
|
||||
code := model.EmailCode{Code: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
}}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
dataCode, _ := json.Marshal(code)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserEmailCodeAdded, Data: dataCode},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstoreWithPw(ctrl, mockEs, false, true, false, false)
|
||||
}
|
||||
func GetMockManipulateUserVerifiedEmail(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
Email: &model.Email{
|
||||
EmailAddress: "EmailAddress",
|
||||
},
|
||||
}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserEmailVerified},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithPhoneCode(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
Phone: &model.Phone{
|
||||
PhoneNumber: "PhoneNumber",
|
||||
},
|
||||
}
|
||||
code := model.PhoneCode{Code: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
}}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
dataCode, _ := json.Marshal(code)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserPhoneCodeAdded, Data: dataCode},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstoreWithPw(ctrl, mockEs, false, false, true, false)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserVerifiedPhone(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
Phone: &model.Phone{
|
||||
PhoneNumber: "PhoneNumber",
|
||||
},
|
||||
}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserPhoneVerified},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
func GetMockManipulateUserFull(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
FirstName: "FirstName",
|
||||
LastName: "LastName",
|
||||
},
|
||||
Password: &model.Password{
|
||||
Secret: &crypto.CryptoValue{Algorithm: "bcrypt", KeyID: "KeyID"},
|
||||
ChangeRequired: true,
|
||||
},
|
||||
Email: &model.Email{
|
||||
EmailAddress: "EmailAddress",
|
||||
},
|
||||
Phone: &model.Phone{
|
||||
PhoneNumber: "PhoneNumber",
|
||||
},
|
||||
Address: &model.Address{
|
||||
Country: "Country",
|
||||
},
|
||||
}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithOTP(ctrl *gomock.Controller) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
}
|
||||
otp := model.OTP{Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
}}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
dataOtp, _ := json.Marshal(otp)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.MfaOtpAdded, Data: dataOtp},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserNoEvents(ctrl *gomock.Controller) *UserEventstore {
|
||||
events := []*es_models.Event{}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
2317
internal/user/repository/eventsourcing/eventstore_test.go
Normal file
2317
internal/user/repository/eventsourcing/eventstore_test.go
Normal file
File diff suppressed because it is too large
Load Diff
77
internal/user/repository/eventsourcing/model/address.go
Normal file
77
internal/user/repository/eventsourcing/model/address.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
)
|
||||
|
||||
type Address struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Country string `json:"country,omitempty"`
|
||||
Locality string `json:"locality,omitempty"`
|
||||
PostalCode string `json:"postalCode,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
StreetAddress string `json:"streetAddress,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Address) Changes(changed *Address) map[string]interface{} {
|
||||
changes := make(map[string]interface{}, 1)
|
||||
if a.Country != changed.Country {
|
||||
changes["country"] = changed.Country
|
||||
}
|
||||
if a.Locality != changed.Locality {
|
||||
changes["locality"] = changed.Locality
|
||||
}
|
||||
if a.PostalCode != changed.PostalCode {
|
||||
changes["postalCode"] = changed.PostalCode
|
||||
}
|
||||
if a.Region != changed.Region {
|
||||
changes["region"] = changed.Region
|
||||
}
|
||||
if a.StreetAddress != changed.StreetAddress {
|
||||
changes["streetAddress"] = changed.StreetAddress
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func AddressFromModel(address *model.Address) *Address {
|
||||
return &Address{
|
||||
ObjectRoot: address.ObjectRoot,
|
||||
Country: address.Country,
|
||||
Locality: address.Locality,
|
||||
PostalCode: address.PostalCode,
|
||||
Region: address.Region,
|
||||
StreetAddress: address.StreetAddress,
|
||||
}
|
||||
}
|
||||
|
||||
func AddressToModel(address *Address) *model.Address {
|
||||
return &model.Address{
|
||||
ObjectRoot: address.ObjectRoot,
|
||||
Country: address.Country,
|
||||
Locality: address.Locality,
|
||||
PostalCode: address.PostalCode,
|
||||
Region: address.Region,
|
||||
StreetAddress: address.StreetAddress,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) appendUserAddressChangedEvent(event *es_models.Event) error {
|
||||
if u.Address == nil {
|
||||
u.Address = new(Address)
|
||||
}
|
||||
return u.Address.setData(event)
|
||||
}
|
||||
|
||||
func (a *Address) setData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-clos0").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-so92s", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
92
internal/user/repository/eventsourcing/model/address_test.go
Normal file
92
internal/user/repository/eventsourcing/model/address_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddressChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existing *Address
|
||||
new *Address
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "all fields changed",
|
||||
args: args{
|
||||
existing: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
|
||||
new: &Address{Country: "CountryChanged", Locality: "LocalityChanged", PostalCode: "PostalCodeChanged", Region: "RegionChanged", StreetAddress: "StreetAddressChanged"},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no fields changed",
|
||||
args: args{
|
||||
existing: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
|
||||
new: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
changes := tt.args.existing.Changes(tt.args.new)
|
||||
if len(changes) != tt.res.changesLen {
|
||||
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUserAddressChangedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
address *Address
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user address event",
|
||||
args: args{
|
||||
user: &User{Address: &Address{Locality: "Locality", Country: "Country"}},
|
||||
address: &Address{Locality: "LocalityChanged", PostalCode: "PostalCode"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{Address: &Address{Locality: "LocalityChanged", Country: "Country", PostalCode: "PostalCode"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.address != nil {
|
||||
data, _ := json.Marshal(tt.args.address)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendUserAddressChangedEvent(tt.args.event)
|
||||
if tt.args.user.Address.Locality != tt.result.Address.Locality {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
if tt.args.user.Address.Country != tt.result.Address.Country {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
if tt.args.user.Address.PostalCode != tt.result.Address.PostalCode {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
102
internal/user/repository/eventsourcing/model/email.go
Normal file
102
internal/user/repository/eventsourcing/model/email.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
EmailAddress string `json:"email,omitempty"`
|
||||
IsEmailVerified bool `json:"-"`
|
||||
|
||||
isEmailUnique bool `json:"-"`
|
||||
}
|
||||
|
||||
type EmailCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Email) Changes(changed *Email) map[string]interface{} {
|
||||
changes := make(map[string]interface{}, 1)
|
||||
if changed.EmailAddress != "" && e.EmailAddress != changed.EmailAddress {
|
||||
changes["email"] = changed.EmailAddress
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func EmailFromModel(email *model.Email) *Email {
|
||||
return &Email{
|
||||
ObjectRoot: email.ObjectRoot,
|
||||
EmailAddress: email.EmailAddress,
|
||||
IsEmailVerified: email.IsEmailVerified,
|
||||
}
|
||||
}
|
||||
|
||||
func EmailToModel(email *Email) *model.Email {
|
||||
return &model.Email{
|
||||
ObjectRoot: email.ObjectRoot,
|
||||
EmailAddress: email.EmailAddress,
|
||||
IsEmailVerified: email.IsEmailVerified,
|
||||
}
|
||||
}
|
||||
|
||||
func EmailCodeFromModel(code *model.EmailCode) *EmailCode {
|
||||
if code == nil {
|
||||
return nil
|
||||
}
|
||||
return &EmailCode{
|
||||
ObjectRoot: code.ObjectRoot,
|
||||
Expiry: code.Expiry,
|
||||
Code: code.Code,
|
||||
}
|
||||
}
|
||||
|
||||
func EmailCodeToModel(code *EmailCode) *model.EmailCode {
|
||||
return &model.EmailCode{
|
||||
ObjectRoot: code.ObjectRoot,
|
||||
Expiry: code.Expiry,
|
||||
Code: code.Code,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) appendUserEmailChangedEvent(event *es_models.Event) error {
|
||||
u.Email = new(Email)
|
||||
return u.Email.setData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendUserEmailCodeAddedEvent(event *es_models.Event) error {
|
||||
u.EmailCode = new(EmailCode)
|
||||
return u.EmailCode.setData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendUserEmailVerifiedEvent() {
|
||||
u.IsEmailVerified = true
|
||||
}
|
||||
|
||||
func (a *Email) setData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-dlo9s").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-sl9xw", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *EmailCode) setData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-lo9s").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-s8uws", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
152
internal/user/repository/eventsourcing/model/email_test.go
Normal file
152
internal/user/repository/eventsourcing/model/email_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEmailChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existing *Email
|
||||
new *Email
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "all fields changed",
|
||||
args: args{
|
||||
existing: &Email{EmailAddress: "Email", IsEmailVerified: true},
|
||||
new: &Email{EmailAddress: "EmailChanged", IsEmailVerified: false},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no fields changed",
|
||||
args: args{
|
||||
existing: &Email{EmailAddress: "Email", IsEmailVerified: true},
|
||||
new: &Email{EmailAddress: "Email", IsEmailVerified: false},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
changes := tt.args.existing.Changes(tt.args.new)
|
||||
if len(changes) != tt.res.changesLen {
|
||||
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUserEmailChangedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
email *Email
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user email event",
|
||||
args: args{
|
||||
user: &User{Email: &Email{EmailAddress: "EmailAddress"}},
|
||||
email: &Email{EmailAddress: "EmailAddressChanged"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{Email: &Email{EmailAddress: "EmailAddressChanged"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.email != nil {
|
||||
data, _ := json.Marshal(tt.args.email)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendUserEmailChangedEvent(tt.args.event)
|
||||
if tt.args.user.Email.EmailAddress != tt.result.Email.EmailAddress {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUserEmailCodeAddedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
code *EmailCode
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user email code added event",
|
||||
args: args{
|
||||
user: &User{Email: &Email{EmailAddress: "EmailAddress"}},
|
||||
code: &EmailCode{Expiry: time.Hour * 1},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{EmailCode: &EmailCode{Expiry: time.Hour * 1}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.code != nil {
|
||||
data, _ := json.Marshal(tt.args.code)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendUserEmailCodeAddedEvent(tt.args.event)
|
||||
if tt.args.user.EmailCode.Expiry != tt.result.EmailCode.Expiry {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUserEmailVerifiedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user email event",
|
||||
args: args{
|
||||
user: &User{Email: &Email{EmailAddress: "EmailAddress"}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{Email: &Email{EmailAddress: "EmailAddress", IsEmailVerified: true}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
tt.args.user.appendUserEmailVerifiedEvent()
|
||||
if tt.args.user.Email.IsEmailVerified != tt.result.Email.IsEmailVerified {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
57
internal/user/repository/eventsourcing/model/mfa.go
Normal file
57
internal/user/repository/eventsourcing/model/mfa.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
)
|
||||
|
||||
type OTP struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Secret *crypto.CryptoValue `json:"otpSecret,omitempty"`
|
||||
State int32 `json:"-"`
|
||||
}
|
||||
|
||||
func OTPFromModel(otp *model.OTP) *OTP {
|
||||
return &OTP{
|
||||
ObjectRoot: otp.ObjectRoot,
|
||||
Secret: otp.Secret,
|
||||
State: int32(otp.State),
|
||||
}
|
||||
}
|
||||
|
||||
func OTPToModel(otp *OTP) *model.OTP {
|
||||
return &model.OTP{
|
||||
ObjectRoot: otp.ObjectRoot,
|
||||
Secret: otp.Secret,
|
||||
State: model.MfaState(otp.State),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) appendOtpAddedEvent(event *es_models.Event) error {
|
||||
u.OTP = &OTP{
|
||||
State: int32(model.MFASTATE_NOTREADY),
|
||||
}
|
||||
return u.OTP.setData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendOtpVerifiedEvent() {
|
||||
u.OTP.State = int32(model.MFASTATE_READY)
|
||||
}
|
||||
|
||||
func (u *User) appendOtpRemovedEvent() {
|
||||
u.OTP = nil
|
||||
}
|
||||
|
||||
func (o *OTP) setData(event *es_models.Event) error {
|
||||
o.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, o); err != nil {
|
||||
logging.Log("EVEN-d9soe").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
109
internal/user/repository/eventsourcing/model/mfa_test.go
Normal file
109
internal/user/repository/eventsourcing/model/mfa_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendMfaOTPAddedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
otp *OTP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user otp event",
|
||||
args: args{
|
||||
user: &User{},
|
||||
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFASTATE_NOTREADY)}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.otp != nil {
|
||||
data, _ := json.Marshal(tt.args.otp)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendOtpAddedEvent(tt.args.event)
|
||||
if tt.args.user.OTP.State != tt.result.OTP.State {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.OTP.State, tt.args.user.OTP.State)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendMfaOTPVerifyEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
otp *OTP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append otp verify event",
|
||||
args: args{
|
||||
user: &User{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}},
|
||||
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFASTATE_READY)}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.otp != nil {
|
||||
data, _ := json.Marshal(tt.args.otp)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendOtpVerifiedEvent()
|
||||
if tt.args.user.OTP.State != tt.result.OTP.State {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.OTP.State, tt.args.user.OTP.State)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendMfaOTPRemoveEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
otp *OTP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append otp verify event",
|
||||
args: args{
|
||||
user: &User{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.user.appendOtpRemovedEvent()
|
||||
if tt.args.user.OTP != nil {
|
||||
t.Errorf("got wrong result: actual: %v ", tt.result.OTP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
84
internal/user/repository/eventsourcing/model/password.go
Normal file
84
internal/user/repository/eventsourcing/model/password.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Password struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"`
|
||||
ChangeRequired bool `json:"changeRequired,omitempty"`
|
||||
}
|
||||
|
||||
type PasswordCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
NotificationType int32 `json:"notificationType,omitempty"`
|
||||
}
|
||||
|
||||
func PasswordFromModel(password *model.Password) *Password {
|
||||
return &Password{
|
||||
ObjectRoot: password.ObjectRoot,
|
||||
Secret: password.SecretCrypto,
|
||||
ChangeRequired: password.ChangeRequired,
|
||||
}
|
||||
}
|
||||
|
||||
func PasswordToModel(password *Password) *model.Password {
|
||||
return &model.Password{
|
||||
ObjectRoot: password.ObjectRoot,
|
||||
SecretCrypto: password.Secret,
|
||||
ChangeRequired: password.ChangeRequired,
|
||||
}
|
||||
}
|
||||
|
||||
func PasswordCodeToModel(code *PasswordCode) *model.PasswordCode {
|
||||
return &model.PasswordCode{
|
||||
ObjectRoot: code.ObjectRoot,
|
||||
Expiry: code.Expiry,
|
||||
Code: code.Code,
|
||||
NotificationType: model.NotificationType(code.NotificationType),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) appendUserPasswordChangedEvent(event *es_models.Event) error {
|
||||
u.Password = new(Password)
|
||||
err := u.Password.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password.ObjectRoot.CreationDate = event.CreationDate
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) appendPasswordSetRequestedEvent(event *es_models.Event) error {
|
||||
u.PasswordCode = new(PasswordCode)
|
||||
return u.PasswordCode.setData(event)
|
||||
}
|
||||
|
||||
func (pw *Password) setData(event *es_models.Event) error {
|
||||
pw.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, pw); err != nil {
|
||||
logging.Log("EVEN-dks93").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-sl9xlo2rsw", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *PasswordCode) setData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-lo0y2").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-q21dr", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAppendUserPasswordChangedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
pw *Password
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append init user code event",
|
||||
args: args{
|
||||
user: &User{},
|
||||
pw: &Password{ChangeRequired: true},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{Password: &Password{ChangeRequired: true}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.pw != nil {
|
||||
data, _ := json.Marshal(tt.args.pw)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendUserPasswordChangedEvent(tt.args.event)
|
||||
if tt.args.user.Password.ChangeRequired != tt.result.Password.ChangeRequired {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendPasswordSetRequestedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
code *PasswordCode
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user phone code added event",
|
||||
args: args{
|
||||
user: &User{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
|
||||
code: &PasswordCode{Expiry: time.Hour * 1},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{PasswordCode: &PasswordCode{Expiry: time.Hour * 1}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.code != nil {
|
||||
data, _ := json.Marshal(tt.args.code)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendPasswordSetRequestedEvent(tt.args.event)
|
||||
if tt.args.user.PasswordCode.Expiry != tt.result.PasswordCode.Expiry {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
100
internal/user/repository/eventsourcing/model/phone.go
Normal file
100
internal/user/repository/eventsourcing/model/phone.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Phone struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
PhoneNumber string `json:"phone,omitempty"`
|
||||
IsPhoneVerified bool `json:"-"`
|
||||
}
|
||||
|
||||
type PhoneCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Phone) Changes(changed *Phone) map[string]interface{} {
|
||||
changes := make(map[string]interface{}, 1)
|
||||
if changed.PhoneNumber != "" && p.PhoneNumber != changed.PhoneNumber {
|
||||
changes["phone"] = changed.PhoneNumber
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func PhoneFromModel(phone *model.Phone) *Phone {
|
||||
return &Phone{
|
||||
ObjectRoot: phone.ObjectRoot,
|
||||
PhoneNumber: phone.PhoneNumber,
|
||||
IsPhoneVerified: phone.IsPhoneVerified,
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneToModel(phone *Phone) *model.Phone {
|
||||
return &model.Phone{
|
||||
ObjectRoot: phone.ObjectRoot,
|
||||
PhoneNumber: phone.PhoneNumber,
|
||||
IsPhoneVerified: phone.IsPhoneVerified,
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneCodeFromModel(code *model.PhoneCode) *PhoneCode {
|
||||
if code == nil {
|
||||
return nil
|
||||
}
|
||||
return &PhoneCode{
|
||||
ObjectRoot: code.ObjectRoot,
|
||||
Expiry: code.Expiry,
|
||||
Code: code.Code,
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneCodeToModel(code *PhoneCode) *model.PhoneCode {
|
||||
return &model.PhoneCode{
|
||||
ObjectRoot: code.ObjectRoot,
|
||||
Expiry: code.Expiry,
|
||||
Code: code.Code,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneChangedEvent(event *es_models.Event) error {
|
||||
u.Phone = new(Phone)
|
||||
return u.Phone.setData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneCodeAddedEvent(event *es_models.Event) error {
|
||||
u.PhoneCode = new(PhoneCode)
|
||||
return u.PhoneCode.setData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneVerifiedEvent() {
|
||||
u.IsPhoneVerified = true
|
||||
}
|
||||
|
||||
func (p *Phone) setData(event *es_models.Event) error {
|
||||
p.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, p); err != nil {
|
||||
logging.Log("EVEN-lco9s").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-lre56", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *PhoneCode) setData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-sk8ws").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-7hdj3", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
152
internal/user/repository/eventsourcing/model/phone_test.go
Normal file
152
internal/user/repository/eventsourcing/model/phone_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPhoneChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existing *Phone
|
||||
new *Phone
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "all fields changed",
|
||||
args: args{
|
||||
existing: &Phone{PhoneNumber: "Phone", IsPhoneVerified: true},
|
||||
new: &Phone{PhoneNumber: "PhoneChanged", IsPhoneVerified: false},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no fields changed",
|
||||
args: args{
|
||||
existing: &Phone{PhoneNumber: "Phone", IsPhoneVerified: true},
|
||||
new: &Phone{PhoneNumber: "Phone", IsPhoneVerified: false},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
changes := tt.args.existing.Changes(tt.args.new)
|
||||
if len(changes) != tt.res.changesLen {
|
||||
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUserPhoneChangedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
phone *Phone
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user phone event",
|
||||
args: args{
|
||||
user: &User{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
|
||||
phone: &Phone{PhoneNumber: "PhoneNumberChanged"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{Phone: &Phone{PhoneNumber: "PhoneNumberChanged"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.phone != nil {
|
||||
data, _ := json.Marshal(tt.args.phone)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendUserPhoneChangedEvent(tt.args.event)
|
||||
if tt.args.user.Phone.PhoneNumber != tt.result.Phone.PhoneNumber {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUserPhoneCodeAddedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
code *PhoneCode
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user phone code added event",
|
||||
args: args{
|
||||
user: &User{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
|
||||
code: &PhoneCode{Expiry: time.Hour * 1},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{PhoneCode: &PhoneCode{Expiry: time.Hour * 1}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.code != nil {
|
||||
data, _ := json.Marshal(tt.args.code)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendUserPhoneCodeAddedEvent(tt.args.event)
|
||||
if tt.args.user.PhoneCode.Expiry != tt.result.PhoneCode.Expiry {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUserPhoneVerifiedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append user phone event",
|
||||
args: args{
|
||||
user: &User{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{Phone: &Phone{PhoneNumber: "PhoneNumber", IsPhoneVerified: true}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
tt.args.user.appendUserPhoneVerifiedEvent()
|
||||
if tt.args.user.Phone.IsPhoneVerified != tt.result.Phone.IsPhoneVerified {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
70
internal/user/repository/eventsourcing/model/profile.go
Normal file
70
internal/user/repository/eventsourcing/model/profile.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
UserName string `json:"userName,omitempty"`
|
||||
FirstName string `json:"firstName,omitempty"`
|
||||
LastName string `json:"lastName,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
|
||||
Gender int32 `json:"gender,omitempty"`
|
||||
|
||||
isUserNameUnique bool `json:"-"`
|
||||
}
|
||||
|
||||
func (p *Profile) Changes(changed *Profile) map[string]interface{} {
|
||||
changes := make(map[string]interface{}, 1)
|
||||
if changed.FirstName != "" && p.FirstName != changed.FirstName {
|
||||
changes["firstName"] = changed.FirstName
|
||||
}
|
||||
if changed.LastName != "" && p.LastName != changed.LastName {
|
||||
changes["lastName"] = changed.LastName
|
||||
}
|
||||
if changed.NickName != p.NickName {
|
||||
changes["nickName"] = changed.NickName
|
||||
}
|
||||
if changed.DisplayName != p.DisplayName {
|
||||
changes["displayName"] = changed.DisplayName
|
||||
}
|
||||
if p.PreferredLanguage != language.Und && changed.PreferredLanguage != p.PreferredLanguage {
|
||||
changes["preferredLanguage"] = changed.PreferredLanguage
|
||||
}
|
||||
if p.Gender > 0 && changed.Gender != p.Gender {
|
||||
changes["gender"] = changed.Gender
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func ProfileFromModel(profile *model.Profile) *Profile {
|
||||
return &Profile{
|
||||
ObjectRoot: profile.ObjectRoot,
|
||||
UserName: profile.UserName,
|
||||
FirstName: profile.FirstName,
|
||||
LastName: profile.LastName,
|
||||
NickName: profile.NickName,
|
||||
DisplayName: profile.DisplayName,
|
||||
PreferredLanguage: profile.PreferredLanguage,
|
||||
Gender: int32(profile.Gender),
|
||||
}
|
||||
}
|
||||
|
||||
func ProfileToModel(profile *Profile) *model.Profile {
|
||||
return &model.Profile{
|
||||
ObjectRoot: profile.ObjectRoot,
|
||||
UserName: profile.UserName,
|
||||
FirstName: profile.FirstName,
|
||||
LastName: profile.LastName,
|
||||
NickName: profile.NickName,
|
||||
DisplayName: profile.DisplayName,
|
||||
PreferredLanguage: profile.PreferredLanguage,
|
||||
Gender: model.Gender(profile.Gender),
|
||||
}
|
||||
}
|
71
internal/user/repository/eventsourcing/model/profile_test.go
Normal file
71
internal/user/repository/eventsourcing/model/profile_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
user_model "github.com/caos/zitadel/internal/user/model"
|
||||
"golang.org/x/text/language"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProfileChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existing *Profile
|
||||
new *Profile
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "all attributes changed",
|
||||
args: args{
|
||||
existing: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
|
||||
new: &Profile{UserName: "UserNameChanged", FirstName: "FirstNameChanged", LastName: "LastNameChanged", NickName: "NickNameChanged", DisplayName: "DisplayNameChanged", PreferredLanguage: language.English, Gender: int32(user_model.GENDER_MALE)},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no changes",
|
||||
args: args{
|
||||
existing: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
|
||||
new: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "username changed",
|
||||
args: args{
|
||||
existing: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
|
||||
new: &Profile{UserName: "UserNameChanged", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty names",
|
||||
args: args{
|
||||
existing: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
|
||||
new: &Profile{UserName: "UserName", FirstName: "", LastName: "", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
changes := tt.args.existing.Changes(tt.args.new)
|
||||
if len(changes) != tt.res.changesLen {
|
||||
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
47
internal/user/repository/eventsourcing/model/types.go
Normal file
47
internal/user/repository/eventsourcing/model/types.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package model
|
||||
|
||||
import "github.com/caos/zitadel/internal/eventstore/models"
|
||||
|
||||
const (
|
||||
UserAggregate models.AggregateType = "user"
|
||||
UserUserNameAggregate models.AggregateType = "user.username"
|
||||
UserEmailAggregate models.AggregateType = "user.email"
|
||||
|
||||
UserAdded models.EventType = "user.added"
|
||||
UserRegistered models.EventType = "user.selfregistered"
|
||||
InitializedUserCodeAdded models.EventType = "user.initialization.code.added"
|
||||
InitializedUserCodeSent models.EventType = "user.initialization.code.sent"
|
||||
|
||||
UserUserNameReserved models.EventType = "user.username.reserved"
|
||||
UserUserNameReleased models.EventType = "user.username.released"
|
||||
UserEmailReserved models.EventType = "user.email.reserved"
|
||||
UserEmailReleased models.EventType = "user.email.released"
|
||||
|
||||
UserLocked models.EventType = "user.locked"
|
||||
UserUnlocked models.EventType = "user.unlocked"
|
||||
UserDeactivated models.EventType = "user.deactivated"
|
||||
UserReactivated models.EventType = "user.reactivated"
|
||||
UserDeleted models.EventType = "user.deleted"
|
||||
|
||||
UserPasswordChanged models.EventType = "user.password.changed"
|
||||
UserPasswordCodeAdded models.EventType = "user.password.code.added"
|
||||
UserPasswordCodeSent models.EventType = "user.password.code.sent"
|
||||
|
||||
UserEmailChanged models.EventType = "user.email.changed"
|
||||
UserEmailVerified models.EventType = "user.email.verified"
|
||||
UserEmailCodeAdded models.EventType = "user.email.code.added"
|
||||
UserEmailCodeSent models.EventType = "user.email.code.sent"
|
||||
|
||||
UserPhoneChanged models.EventType = "user.phone.changed"
|
||||
UserPhoneVerified models.EventType = "user.phone.verified"
|
||||
UserPhoneCodeAdded models.EventType = "user.phone.code.added"
|
||||
UserPhoneCodeSent models.EventType = "user.phone.code.sent"
|
||||
|
||||
UserProfileChanged models.EventType = "user.profile.changed"
|
||||
UserAddressChanged models.EventType = "user.address.changed"
|
||||
|
||||
MfaOtpAdded models.EventType = "user.mfa.otp.added"
|
||||
MfaOtpVerified models.EventType = "user.mfa.otp.verified"
|
||||
MfaOtpRemoved models.EventType = "user.mfa.otp.removed"
|
||||
MfaInitSkipped models.EventType = "user.mfa.init.skipped"
|
||||
)
|
246
internal/user/repository/eventsourcing/model/user.go
Normal file
246
internal/user/repository/eventsourcing/model/user.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UserVersion = "v1"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
es_models.ObjectRoot
|
||||
State int32 `json:"-"`
|
||||
*Password
|
||||
*Profile
|
||||
*Email
|
||||
*Phone
|
||||
*Address
|
||||
InitCode *InitUserCode
|
||||
EmailCode *EmailCode
|
||||
PhoneCode *PhoneCode
|
||||
PasswordCode *PasswordCode
|
||||
OTP *OTP
|
||||
}
|
||||
|
||||
type InitUserCode struct {
|
||||
es_models.ObjectRoot
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
}
|
||||
|
||||
func UserFromModel(user *model.User) *User {
|
||||
converted := &User{
|
||||
ObjectRoot: user.ObjectRoot,
|
||||
State: int32(user.State),
|
||||
}
|
||||
if user.Password != nil {
|
||||
converted.Password = PasswordFromModel(user.Password)
|
||||
}
|
||||
if user.Profile != nil {
|
||||
converted.Profile = ProfileFromModel(user.Profile)
|
||||
}
|
||||
if user.Email != nil {
|
||||
converted.Email = EmailFromModel(user.Email)
|
||||
}
|
||||
if user.Phone != nil {
|
||||
converted.Phone = PhoneFromModel(user.Phone)
|
||||
}
|
||||
if user.Address != nil {
|
||||
converted.Address = AddressFromModel(user.Address)
|
||||
}
|
||||
if user.OTP != nil {
|
||||
converted.OTP = OTPFromModel(user.OTP)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func UserToModel(user *User) *model.User {
|
||||
converted := &model.User{
|
||||
ObjectRoot: user.ObjectRoot,
|
||||
State: model.UserState(user.State),
|
||||
}
|
||||
if user.Password != nil {
|
||||
converted.Password = PasswordToModel(user.Password)
|
||||
}
|
||||
if user.Profile != nil {
|
||||
converted.Profile = ProfileToModel(user.Profile)
|
||||
}
|
||||
if user.Email != nil {
|
||||
converted.Email = EmailToModel(user.Email)
|
||||
}
|
||||
if user.Phone != nil {
|
||||
converted.Phone = PhoneToModel(user.Phone)
|
||||
}
|
||||
if user.Address != nil {
|
||||
converted.Address = AddressToModel(user.Address)
|
||||
}
|
||||
if user.InitCode != nil {
|
||||
converted.InitCode = InitCodeToModel(user.InitCode)
|
||||
}
|
||||
if user.EmailCode != nil {
|
||||
converted.EmailCode = EmailCodeToModel(user.EmailCode)
|
||||
}
|
||||
if user.PhoneCode != nil {
|
||||
converted.PhoneCode = PhoneCodeToModel(user.PhoneCode)
|
||||
}
|
||||
if user.PasswordCode != nil {
|
||||
converted.PasswordCode = PasswordCodeToModel(user.PasswordCode)
|
||||
}
|
||||
if user.OTP != nil {
|
||||
converted.OTP = OTPToModel(user.OTP)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func InitCodeFromModel(code *model.InitUserCode) *InitUserCode {
|
||||
if code == nil {
|
||||
return nil
|
||||
}
|
||||
return &InitUserCode{
|
||||
ObjectRoot: code.ObjectRoot,
|
||||
Expiry: code.Expiry,
|
||||
Code: code.Code,
|
||||
}
|
||||
}
|
||||
|
||||
func InitCodeToModel(code *InitUserCode) *model.InitUserCode {
|
||||
return &model.InitUserCode{
|
||||
ObjectRoot: code.ObjectRoot,
|
||||
Expiry: code.Expiry,
|
||||
Code: code.Code,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *User) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
if err := p.AppendEvent(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) AppendEvent(event *es_models.Event) (err error) {
|
||||
u.ObjectRoot.AppendEvent(event)
|
||||
switch event.Type {
|
||||
case UserAdded,
|
||||
UserRegistered,
|
||||
UserProfileChanged:
|
||||
u.setData(event)
|
||||
case UserDeactivated:
|
||||
u.appendDeactivatedEvent()
|
||||
case UserReactivated:
|
||||
u.appendReactivatedEvent()
|
||||
case UserLocked:
|
||||
u.appendLockedEvent()
|
||||
case UserUnlocked:
|
||||
u.appendUnlockedEvent()
|
||||
case InitializedUserCodeAdded:
|
||||
u.appendInitUsercodeCreatedEvent(event)
|
||||
case UserPasswordChanged:
|
||||
err = u.appendUserPasswordChangedEvent(event)
|
||||
case UserPasswordCodeAdded:
|
||||
err = u.appendPasswordSetRequestedEvent(event)
|
||||
case UserEmailChanged:
|
||||
err = u.appendUserEmailChangedEvent(event)
|
||||
case UserEmailCodeAdded:
|
||||
err = u.appendUserEmailCodeAddedEvent(event)
|
||||
case UserEmailVerified:
|
||||
u.appendUserEmailVerifiedEvent()
|
||||
case UserPhoneChanged:
|
||||
err = u.appendUserPhoneChangedEvent(event)
|
||||
case UserPhoneCodeAdded:
|
||||
err = u.appendUserPhoneCodeAddedEvent(event)
|
||||
case UserPhoneVerified:
|
||||
u.appendUserPhoneVerifiedEvent()
|
||||
case UserAddressChanged:
|
||||
err = u.appendUserAddressChangedEvent(event)
|
||||
case MfaOtpAdded:
|
||||
err = u.appendOtpAddedEvent(event)
|
||||
case MfaOtpVerified:
|
||||
u.appendOtpVerifiedEvent()
|
||||
case MfaOtpRemoved:
|
||||
u.appendOtpRemovedEvent()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.ComputeObject()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) ComputeObject() {
|
||||
if u.State == 0 {
|
||||
if u.Email != nil && u.IsEmailVerified {
|
||||
u.State = int32(model.USERSTATE_ACTIVE)
|
||||
} else {
|
||||
u.State = int32(model.USERSTATE_INITIAL)
|
||||
}
|
||||
}
|
||||
if u.Password != nil && u.Password.ObjectRoot.IsZero() {
|
||||
u.Password.ObjectRoot = u.ObjectRoot
|
||||
}
|
||||
if u.Profile != nil && u.Profile.ObjectRoot.IsZero() {
|
||||
u.Profile.ObjectRoot = u.ObjectRoot
|
||||
}
|
||||
if u.Email != nil && u.Email.ObjectRoot.IsZero() {
|
||||
u.Email.ObjectRoot = u.ObjectRoot
|
||||
}
|
||||
if u.Phone != nil && u.Phone.ObjectRoot.IsZero() {
|
||||
u.Phone.ObjectRoot = u.ObjectRoot
|
||||
}
|
||||
if u.Address != nil && u.Address.ObjectRoot.IsZero() {
|
||||
u.Address.ObjectRoot = u.ObjectRoot
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) setData(event *es_models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, u); err != nil {
|
||||
logging.Log("EVEN-8ujgd").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-sj4jd", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) appendDeactivatedEvent() {
|
||||
u.State = int32(model.USERSTATE_INACTIVE)
|
||||
}
|
||||
|
||||
func (u *User) appendReactivatedEvent() {
|
||||
u.State = int32(model.USERSTATE_ACTIVE)
|
||||
}
|
||||
|
||||
func (u *User) appendLockedEvent() {
|
||||
u.State = int32(model.USERSTATE_LOCKED)
|
||||
}
|
||||
|
||||
func (u *User) appendUnlockedEvent() {
|
||||
u.State = int32(model.USERSTATE_ACTIVE)
|
||||
}
|
||||
|
||||
func (u *User) appendInitUsercodeCreatedEvent(event *es_models.Event) error {
|
||||
initCode := new(InitUserCode)
|
||||
err := initCode.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
initCode.ObjectRoot.CreationDate = event.CreationDate
|
||||
u.InitCode = initCode
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *InitUserCode) setData(event *es_models.Event) error {
|
||||
c.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, c); err != nil {
|
||||
logging.Log("EVEN-7duwe").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "MODEL-lo34s", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
152
internal/user/repository/eventsourcing/model/user_test.go
Normal file
152
internal/user/repository/eventsourcing/model/user_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAppendDeactivatedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append deactivate event",
|
||||
args: args{
|
||||
user: &User{},
|
||||
},
|
||||
result: &User{State: int32(model.USERSTATE_INACTIVE)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.user.appendDeactivatedEvent()
|
||||
if tt.args.user.State != tt.result.State {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendReactivatedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append reactivate event",
|
||||
args: args{
|
||||
user: &User{},
|
||||
},
|
||||
result: &User{State: int32(model.USERSTATE_ACTIVE)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.user.appendReactivatedEvent()
|
||||
if tt.args.user.State != tt.result.State {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendLockEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append lock event",
|
||||
args: args{
|
||||
user: &User{},
|
||||
},
|
||||
result: &User{State: int32(model.USERSTATE_LOCKED)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.user.appendLockedEvent()
|
||||
if tt.args.user.State != tt.result.State {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUnlockEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append unlock event",
|
||||
args: args{
|
||||
user: &User{},
|
||||
},
|
||||
result: &User{State: int32(model.USERSTATE_ACTIVE)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.user.appendUnlockedEvent()
|
||||
if tt.args.user.State != tt.result.State {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendInitUserCodeEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *User
|
||||
code *InitUserCode
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *User
|
||||
}{
|
||||
{
|
||||
name: "append init user code event",
|
||||
args: args{
|
||||
user: &User{},
|
||||
code: &InitUserCode{Expiry: time.Hour * 30},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &User{InitCode: &InitUserCode{Expiry: time.Hour * 30}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.code != nil {
|
||||
data, _ := json.Marshal(tt.args.code)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendInitUsercodeCreatedEvent(tt.args.event)
|
||||
if tt.args.user.InitCode.Expiry != tt.result.InitCode.Expiry {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
348
internal/user/repository/eventsourcing/user.go
Normal file
348
internal/user/repository/eventsourcing/user.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
func UserByIDQuery(id string, latestSequence uint64) (*es_models.SearchQuery, error) {
|
||||
if id == "" {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d8isw", "id should be filled")
|
||||
}
|
||||
return UserQuery(latestSequence).
|
||||
AggregateIDFilter(id), nil
|
||||
}
|
||||
|
||||
func UserQuery(latestSequence uint64) *es_models.SearchQuery {
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(model.UserAggregate).
|
||||
LatestSequenceFilter(latestSequence)
|
||||
}
|
||||
|
||||
func UserAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User) (*es_models.Aggregate, error) {
|
||||
if user == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dis83", "existing user should not be nil")
|
||||
}
|
||||
return aggCreator.NewAggregate(ctx, user.AggregateID, model.UserAggregate, model.UserVersion, user.Sequence)
|
||||
}
|
||||
|
||||
func UserAggregateOverwriteContext(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, resourceOwnerID string, userID string) (*es_models.Aggregate, error) {
|
||||
if user == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dis83", "existing user should not be nil")
|
||||
}
|
||||
|
||||
return aggCreator.NewAggregate(ctx, user.AggregateID, model.UserAggregate, model.UserVersion, user.Sequence, es_models.OverwriteResourceOwner(resourceOwnerID), es_models.OverwriteEditorUser(userID))
|
||||
}
|
||||
|
||||
func UserCreateAggregate(aggCreator *es_models.AggregateCreator, user *model.User, initCode *model.InitUserCode, phoneCode *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if user == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user should not be nil")
|
||||
}
|
||||
|
||||
agg, err := UserAggregate(ctx, aggCreator, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agg, err = agg.AppendEvent(model.UserAdded, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Email != nil && user.EmailAddress != "" && user.IsEmailVerified {
|
||||
agg, err = agg.AppendEvent(model.UserEmailVerified, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Phone != nil && user.PhoneNumber != "" && user.IsPhoneVerified {
|
||||
agg, err = agg.AppendEvent(model.UserPhoneVerified, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Password != nil {
|
||||
agg, err = agg.AppendEvent(model.UserPasswordCodeAdded, user.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if initCode != nil {
|
||||
agg, err = agg.AppendEvent(model.InitializedUserCodeAdded, initCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if phoneCode != nil {
|
||||
agg, err = agg.AppendEvent(model.UserPhoneCodeAdded, phoneCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return agg, err
|
||||
}
|
||||
}
|
||||
|
||||
func UserRegisterAggregate(aggCreator *es_models.AggregateCreator, user *model.User, resourceOwner string, emailCode *model.EmailCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if user == nil || resourceOwner == "" || emailCode == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user, resourceowner, emailcode should not be nothing")
|
||||
}
|
||||
|
||||
agg, err := UserAggregateOverwriteContext(ctx, aggCreator, user, resourceOwner, user.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agg, err = agg.AppendEvent(model.UserRegistered, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserEmailCodeAdded, emailCode)
|
||||
}
|
||||
}
|
||||
|
||||
func UserDeactivateAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return userStateAggregate(aggCreator, user, model.UserDeactivated)
|
||||
}
|
||||
|
||||
func UserReactivateAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return userStateAggregate(aggCreator, user, model.UserReactivated)
|
||||
}
|
||||
|
||||
func UserLockAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return userStateAggregate(aggCreator, user, model.UserLocked)
|
||||
}
|
||||
|
||||
func UserUnlockAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return userStateAggregate(aggCreator, user, model.UserUnlocked)
|
||||
}
|
||||
|
||||
func userStateAggregate(aggCreator *es_models.AggregateCreator, user *model.User, state es_models.EventType) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(state, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func UserInitCodeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, code *model.InitUserCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if code == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d8i23", "code should not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.InitializedUserCodeAdded, code)
|
||||
}
|
||||
}
|
||||
|
||||
func SkipMfaAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.MfaInitSkipped, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func PasswordChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, password *model.Password) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if password == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d9832", "password should not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPasswordChanged, password)
|
||||
}
|
||||
}
|
||||
|
||||
func RequestSetPassword(aggCreator *es_models.AggregateCreator, existing *model.User, request *model.PasswordCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if request == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d8ei2", "password set request should not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPasswordCodeAdded, request)
|
||||
}
|
||||
}
|
||||
|
||||
func ProfileChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, profile *model.Profile) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if profile == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dhr74", "profile should not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changes := existing.Profile.Changes(profile)
|
||||
return agg.AppendEvent(model.UserProfileChanged, changes)
|
||||
}
|
||||
}
|
||||
|
||||
func EmailChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, email *model.Email, code *model.EmailCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if email == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dki8s", "email should not be nil")
|
||||
}
|
||||
if (!email.IsEmailVerified && code == nil) || (email.IsEmailVerified && code != nil) {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-id934", "email has to be verified or code must be sent")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changes := existing.Email.Changes(email)
|
||||
agg, err = agg.AppendEvent(model.UserEmailChanged, changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.Email == nil {
|
||||
existing.Email = new(model.Email)
|
||||
}
|
||||
if email.IsEmailVerified {
|
||||
return agg.AppendEvent(model.UserEmailVerified, code)
|
||||
}
|
||||
if code != nil {
|
||||
return agg.AppendEvent(model.UserEmailCodeAdded, code)
|
||||
}
|
||||
return agg, nil
|
||||
}
|
||||
}
|
||||
func EmailVerifiedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserEmailVerified, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func EmailVerificationCodeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, code *model.EmailCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if code == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dki8s", "code should not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserEmailCodeAdded, code)
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, phone *model.Phone, code *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if phone == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dkso3", "phone should not be nil")
|
||||
}
|
||||
if (!phone.IsPhoneVerified && code == nil) || (phone.IsPhoneVerified && code != nil) {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dksi8", "phone has to be verified or code must be sent")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.Phone == nil {
|
||||
existing.Phone = new(model.Phone)
|
||||
}
|
||||
changes := existing.Phone.Changes(phone)
|
||||
agg, err = agg.AppendEvent(model.UserPhoneChanged, changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if phone.IsPhoneVerified {
|
||||
return agg.AppendEvent(model.UserPhoneVerified, code)
|
||||
}
|
||||
if code != nil {
|
||||
return agg.AppendEvent(model.UserPhoneCodeAdded, code)
|
||||
}
|
||||
return agg, nil
|
||||
}
|
||||
}
|
||||
func PhoneVerifiedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPhoneVerified, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneVerificationCodeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, code *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if code == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dsue2", "code should not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPhoneCodeAdded, code)
|
||||
}
|
||||
}
|
||||
|
||||
func AddressChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, address *model.Address) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if address == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dkx9s", "address should not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.Address == nil {
|
||||
existing.Address = new(model.Address)
|
||||
}
|
||||
changes := existing.Address.Changes(address)
|
||||
return agg.AppendEvent(model.UserAddressChanged, changes)
|
||||
}
|
||||
}
|
||||
|
||||
func MfaOTPAddAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, otp *model.OTP) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if otp == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dkx9s", "otp should not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.MfaOtpAdded, otp)
|
||||
}
|
||||
}
|
||||
|
||||
func MfaOTPVerifyAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.MfaOtpVerified, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func MfaOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.MfaOtpRemoved, nil)
|
||||
}
|
||||
}
|
1574
internal/user/repository/eventsourcing/user_test.go
Normal file
1574
internal/user/repository/eventsourcing/user_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user