mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:57:33 +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:
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user