mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 07:47:32 +00:00
chore: move the go code into a subfolder
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
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 (u *Human) 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 zerrors.ThrowInternal(err, "MODEL-so92s", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
func TestAddressChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existingAddress *Address
|
||||
newAddress *Address
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "all fields changed",
|
||||
args: args{
|
||||
existingAddress: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
|
||||
newAddress: &Address{Country: "CountryChanged", Locality: "LocalityChanged", PostalCode: "PostalCodeChanged", Region: "RegionChanged", StreetAddress: "StreetAddressChanged"},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no fields changed",
|
||||
args: args{
|
||||
existingAddress: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
|
||||
newAddress: &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.existingAddress.Changes(tt.args.newAddress)
|
||||
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 *Human
|
||||
address *Address
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user address event",
|
||||
args: args{
|
||||
user: &Human{Address: &Address{Locality: "Locality", Country: "Country"}},
|
||||
address: &Address{Locality: "LocalityChanged", PostalCode: "PostalCode"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type AuthRequest struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
UserAgentID string `json:"userAgentID,omitempty"`
|
||||
SelectedIDPConfigID string `json:"selectedIDPConfigID,omitempty"`
|
||||
*BrowserInfo
|
||||
}
|
||||
|
||||
type BrowserInfo struct {
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
AcceptLanguage string `json:"acceptLanguage,omitempty"`
|
||||
RemoteIP net.IP `json:"remoteIP,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AuthRequest) SetData(event eventstore.Event) error {
|
||||
if err := event.Unmarshal(a); err != nil {
|
||||
logging.Log("EVEN-T5df6").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-yGmhh", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
EmailAddress string `json:"email,omitempty"`
|
||||
IsEmailVerified 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 (u *Human) appendUserEmailChangedEvent(event *es_models.Event) error {
|
||||
u.Email = new(Email)
|
||||
return u.Email.setData(event)
|
||||
}
|
||||
|
||||
func (u *Human) appendUserEmailCodeAddedEvent(event *es_models.Event) error {
|
||||
u.EmailCode = new(EmailCode)
|
||||
return u.EmailCode.SetData(event)
|
||||
}
|
||||
|
||||
func (u *Human) 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 zerrors.ThrowInternal(err, "MODEL-sl9xw", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *EmailCode) SetData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
a.CreationDate = event.CreationDate
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-lo9s").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-s8uws", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,153 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
func TestEmailChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existingEmail *Email
|
||||
new *Email
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "all fields changed",
|
||||
args: args{
|
||||
existingEmail: &Email{EmailAddress: "Email", IsEmailVerified: true},
|
||||
new: &Email{EmailAddress: "EmailChanged", IsEmailVerified: false},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no fields changed",
|
||||
args: args{
|
||||
existingEmail: &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.existingEmail.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 *Human
|
||||
email *Email
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user email event",
|
||||
args: args{
|
||||
user: &Human{Email: &Email{EmailAddress: "EmailAddress"}},
|
||||
email: &Email{EmailAddress: "EmailAddressChanged"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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 *Human
|
||||
code *EmailCode
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user email code added event",
|
||||
args: args{
|
||||
user: &Human{Email: &Email{EmailAddress: "EmailAddress"}},
|
||||
code: &EmailCode{Expiry: time.Hour * 1},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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 *Human
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user email event",
|
||||
args: args{
|
||||
user: &Human{Email: &Email{EmailAddress: "EmailAddress"}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type ExternalIDP struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
IDPConfigID string `json:"idpConfigId,omitempty"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
}
|
||||
|
||||
func GetExternalIDP(idps []*ExternalIDP, id string) (int, *ExternalIDP) {
|
||||
for i, idp := range idps {
|
||||
if idp.UserID == id {
|
||||
return i, idp
|
||||
}
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
func (u *Human) appendExternalIDPAddedEvent(event *es_models.Event) error {
|
||||
idp := new(ExternalIDP)
|
||||
err := idp.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idp.ObjectRoot.CreationDate = event.CreationDate
|
||||
u.ExternalIDPs = append(u.ExternalIDPs, idp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendExternalIDPRemovedEvent(event *es_models.Event) error {
|
||||
idp := new(ExternalIDP)
|
||||
err := idp.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i, externalIdp := GetExternalIDP(u.ExternalIDPs, idp.UserID); externalIdp != nil {
|
||||
u.ExternalIDPs[i] = u.ExternalIDPs[len(u.ExternalIDPs)-1]
|
||||
u.ExternalIDPs[len(u.ExternalIDPs)-1] = nil
|
||||
u.ExternalIDPs = u.ExternalIDPs[:len(u.ExternalIDPs)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pw *ExternalIDP) setData(event *es_models.Event) error {
|
||||
pw.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, pw); err != nil {
|
||||
logging.Log("EVEN-Msi9d").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-A9osf", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
func TestAppendExternalIDPAddedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
externalIDP *ExternalIDP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append external idp added event",
|
||||
args: args{
|
||||
user: &Human{},
|
||||
externalIDP: &ExternalIDP{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{ExternalIDPs: []*ExternalIDP{{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"}}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.externalIDP != nil {
|
||||
data, _ := json.Marshal(tt.args.externalIDP)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendExternalIDPAddedEvent(tt.args.event)
|
||||
if len(tt.args.user.ExternalIDPs) == 0 {
|
||||
t.Error("got wrong result expected external idps on user ")
|
||||
}
|
||||
if tt.args.user.ExternalIDPs[0].UserID != tt.result.ExternalIDPs[0].UserID {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].UserID, tt.args.user.ExternalIDPs[0].UserID)
|
||||
}
|
||||
if tt.args.user.ExternalIDPs[0].IDPConfigID != tt.result.ExternalIDPs[0].IDPConfigID {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].IDPConfigID, tt.args.user.ExternalIDPs[0].IDPConfigID)
|
||||
}
|
||||
if tt.args.user.ExternalIDPs[0].DisplayName != tt.result.ExternalIDPs[0].DisplayName {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.ExternalIDPs[0].DisplayName, tt.args.user.ExternalIDPs[0].IDPConfigID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendExternalIDPRemovedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
externalIDP *ExternalIDP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append external idp removed event",
|
||||
args: args{
|
||||
user: &Human{
|
||||
ExternalIDPs: []*ExternalIDP{
|
||||
{IDPConfigID: "IDPConfigID", UserID: "UserID", DisplayName: "DisplayName"},
|
||||
}},
|
||||
externalIDP: &ExternalIDP{UserID: "UserID"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.externalIDP != nil {
|
||||
data, _ := json.Marshal(tt.args.externalIDP)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendExternalIDPRemovedEvent(tt.args.event)
|
||||
if len(tt.args.user.ExternalIDPs) != 0 {
|
||||
t.Error("got wrong result expected 0 external idps on user ")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
54
apps/api/internal/user/repository/eventsourcing/model/otp.go
Normal file
54
apps/api/internal/user/repository/eventsourcing/model/otp.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type OTP struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Secret *crypto.CryptoValue `json:"otpSecret,omitempty"`
|
||||
State int32 `json:"-"`
|
||||
}
|
||||
|
||||
type OTPVerified struct {
|
||||
UserAgentID string `json:"userAgentID,omitempty"`
|
||||
}
|
||||
|
||||
func (u *Human) appendOTPAddedEvent(event eventstore.Event) error {
|
||||
u.OTP = &OTP{
|
||||
State: int32(model.MFAStateNotReady),
|
||||
}
|
||||
return u.OTP.setData(event)
|
||||
}
|
||||
|
||||
func (u *Human) appendOTPVerifiedEvent() {
|
||||
u.OTP.State = int32(model.MFAStateReady)
|
||||
}
|
||||
|
||||
func (u *Human) appendOTPRemovedEvent() {
|
||||
u.OTP = nil
|
||||
}
|
||||
|
||||
func (o *OTP) setData(event eventstore.Event) error {
|
||||
o.ObjectRoot.AppendEvent(event)
|
||||
if err := event.Unmarshal(o); err != nil {
|
||||
logging.Log("EVEN-d9soe").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OTPVerified) SetData(event eventstore.Event) error {
|
||||
if err := event.Unmarshal(o); err != nil {
|
||||
logging.Log("EVEN-BF421").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-GB6hj", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
)
|
||||
|
||||
func TestAppendMFAOTPAddedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
otp *OTP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user otp event",
|
||||
args: args{
|
||||
user: &Human{},
|
||||
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFAStateNotReady)}},
|
||||
},
|
||||
}
|
||||
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 *Human
|
||||
otp *OTP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append otp verify event",
|
||||
args: args{
|
||||
user: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}},
|
||||
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFAStateReady)}},
|
||||
},
|
||||
}
|
||||
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 *Human
|
||||
otp *OTP
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append otp verify event",
|
||||
args: args{
|
||||
user: &Human{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Password struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"`
|
||||
EncodedHash string `json:"encodedHash,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"`
|
||||
}
|
||||
|
||||
type PasswordChange struct {
|
||||
Password
|
||||
UserAgentID string `json:"userAgentID,omitempty"`
|
||||
}
|
||||
|
||||
func (u *Human) appendUserPasswordChangedEvent(event eventstore.Event) error {
|
||||
u.Password = new(Password)
|
||||
err := u.Password.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password.ObjectRoot.CreationDate = event.CreatedAt()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendPasswordSetRequestedEvent(event eventstore.Event) error {
|
||||
u.PasswordCode = new(PasswordCode)
|
||||
return u.PasswordCode.SetData(event)
|
||||
}
|
||||
|
||||
func (pw *Password) setData(event eventstore.Event) error {
|
||||
pw.ObjectRoot.AppendEvent(event)
|
||||
if err := event.Unmarshal(pw); err != nil {
|
||||
logging.Log("EVEN-dks93").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-sl9xlo2rsw", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PasswordCode) SetData(event eventstore.Event) error {
|
||||
c.ObjectRoot.AppendEvent(event)
|
||||
c.CreationDate = event.CreatedAt()
|
||||
if err := event.Unmarshal(c); err != nil {
|
||||
logging.Log("EVEN-lo0y2").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-q21dr", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pw *PasswordChange) SetData(event eventstore.Event) error {
|
||||
if err := event.Unmarshal(pw); err != nil {
|
||||
logging.Log("EVEN-ADs31").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-BDd32", "could not unmarshal event")
|
||||
}
|
||||
pw.ObjectRoot.AppendEvent(event)
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
func TestAppendUserPasswordChangedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
pw *Password
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append init user code event",
|
||||
args: args{
|
||||
user: &Human{},
|
||||
pw: &Password{ChangeRequired: true},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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 *Human
|
||||
code *PasswordCode
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user phone code added event",
|
||||
args: args{
|
||||
user: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
|
||||
code: &PasswordCode{Expiry: time.Hour * 1},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
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 (u *Human) appendUserPhoneChangedEvent(event *es_models.Event) error {
|
||||
u.Phone = new(Phone)
|
||||
return u.Phone.setData(event)
|
||||
}
|
||||
|
||||
func (u *Human) appendUserPhoneCodeAddedEvent(event *es_models.Event) error {
|
||||
u.PhoneCode = new(PhoneCode)
|
||||
return u.PhoneCode.SetData(event)
|
||||
}
|
||||
|
||||
func (u *Human) appendUserPhoneVerifiedEvent() {
|
||||
u.IsPhoneVerified = true
|
||||
}
|
||||
|
||||
func (u *Human) appendUserPhoneRemovedEvent() {
|
||||
u.Phone = nil
|
||||
u.PhoneCode = 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-lco9s").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-lre56", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PhoneCode) SetData(event *es_models.Event) error {
|
||||
c.ObjectRoot.AppendEvent(event)
|
||||
c.CreationDate = event.CreationDate
|
||||
if err := json.Unmarshal(event.Data, c); err != nil {
|
||||
logging.Log("EVEN-sk8ws").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-7hdj3", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,153 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
func TestPhoneChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existingPhone *Phone
|
||||
newPhone *Phone
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "all fields changed",
|
||||
args: args{
|
||||
existingPhone: &Phone{PhoneNumber: "Phone", IsPhoneVerified: true},
|
||||
newPhone: &Phone{PhoneNumber: "PhoneChanged", IsPhoneVerified: false},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no fields changed",
|
||||
args: args{
|
||||
existingPhone: &Phone{PhoneNumber: "Phone", IsPhoneVerified: true},
|
||||
newPhone: &Phone{PhoneNumber: "Phone", IsPhoneVerified: false},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
changes := tt.args.existingPhone.Changes(tt.args.newPhone)
|
||||
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 *Human
|
||||
phone *Phone
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user phone event",
|
||||
args: args{
|
||||
user: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
|
||||
phone: &Phone{PhoneNumber: "PhoneNumberChanged"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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 *Human
|
||||
code *PhoneCode
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user phone code added event",
|
||||
args: args{
|
||||
user: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
|
||||
code: &PhoneCode{Expiry: time.Hour * 1},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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 *Human
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user phone event",
|
||||
args: args{
|
||||
user: &Human{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
FirstName string `json:"firstName,omitempty"`
|
||||
LastName string `json:"lastName,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
PreferredLanguage LanguageTag `json:"preferredLanguage,omitempty"`
|
||||
Gender int32 `json:"gender,omitempty"`
|
||||
}
|
||||
|
||||
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 != changed.DisplayName {
|
||||
changes["displayName"] = changed.DisplayName
|
||||
}
|
||||
if language.Tag(changed.PreferredLanguage) != language.Und && changed.PreferredLanguage != p.PreferredLanguage {
|
||||
changes["preferredLanguage"] = changed.PreferredLanguage
|
||||
}
|
||||
if changed.Gender != p.Gender {
|
||||
changes["gender"] = changed.Gender
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
type LanguageTag language.Tag
|
||||
|
||||
func (t *LanguageTag) UnmarshalJSON(data []byte) error {
|
||||
var tag string
|
||||
err := json.Unmarshal(data, &tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = LanguageTag(language.Make(tag))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t LanguageTag) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(language.Tag(t))
|
||||
}
|
||||
|
||||
func (t *LanguageTag) MarshalBinary() ([]byte, error) {
|
||||
if t == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []byte(language.Tag(*t).String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary modifies the receiver so it must take a pointer receiver.
|
||||
func (t *LanguageTag) UnmarshalBinary(data []byte) error {
|
||||
*t = LanguageTag(language.Make(string(data)))
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
user_model "github.com/zitadel/zitadel/internal/user/model"
|
||||
)
|
||||
|
||||
func TestProfileChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existingProfile *Profile
|
||||
newProfile *Profile
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "all attributes changed",
|
||||
args: args{
|
||||
existingProfile: &Profile{FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
|
||||
newProfile: &Profile{FirstName: "FirstNameChanged", LastName: "LastNameChanged", NickName: "NickNameChanged", DisplayName: "DisplayNameChanged", PreferredLanguage: LanguageTag(language.English), Gender: int32(user_model.GenderMale)},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no changes",
|
||||
args: args{
|
||||
existingProfile: &Profile{FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
|
||||
newProfile: &Profile{FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty names",
|
||||
args: args{
|
||||
existingProfile: &Profile{FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
|
||||
newProfile: &Profile{FirstName: "", LastName: "", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: LanguageTag(language.German), Gender: int32(user_model.GenderFemale)},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
changes := tt.args.existingProfile.Changes(tt.args.newProfile)
|
||||
if len(changes) != tt.res.changesLen {
|
||||
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
user_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
TokenID string `json:"tokenId" gorm:"column:token_id"`
|
||||
ApplicationID string `json:"applicationId" gorm:"column:application_id"`
|
||||
UserAgentID string `json:"userAgentId" gorm:"column:user_agent_id"`
|
||||
Audience database.TextArray[string] `json:"audience" gorm:"column:audience"`
|
||||
Scopes database.TextArray[string] `json:"scopes" gorm:"column:scopes"`
|
||||
Expiration time.Time `json:"expiration" gorm:"column:expiration"`
|
||||
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
||||
}
|
||||
|
||||
func (t *Token) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
if err := t.AppendEvent(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Token) AppendEvent(event *es_models.Event) error {
|
||||
if event.Typ == user_repo.UserTokenAddedType {
|
||||
err := t.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CreationDate = event.CreationDate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Token) setData(event *es_models.Event) error {
|
||||
t.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, t); err != nil {
|
||||
logging.Log("EVEN-4Fm9s").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-5Gms9", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
111
apps/api/internal/user/repository/eventsourcing/model/user.go
Normal file
111
apps/api/internal/user/repository/eventsourcing/model/user.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
UserVersion = "v2"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
es_models.ObjectRoot
|
||||
State int32 `json:"-"`
|
||||
UserName string `json:"userName"`
|
||||
|
||||
*Human
|
||||
*Machine
|
||||
}
|
||||
|
||||
func (u *User) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
if err := u.AppendEvent(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) AppendEvent(event *es_models.Event) error {
|
||||
u.ObjectRoot.AppendEvent(event)
|
||||
|
||||
switch event.Type() {
|
||||
case user.UserV1AddedType,
|
||||
user.HumanAddedType,
|
||||
user.MachineAddedEventType,
|
||||
user.UserV1RegisteredType,
|
||||
user.HumanRegisteredType,
|
||||
user.UserV1ProfileChangedType,
|
||||
user.UserDomainClaimedType,
|
||||
user.UserUserNameChangedType:
|
||||
err := u.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case user.UserDeactivatedType:
|
||||
u.appendDeactivatedEvent()
|
||||
case user.UserReactivatedType:
|
||||
u.appendReactivatedEvent()
|
||||
case user.UserLockedType:
|
||||
u.appendLockedEvent()
|
||||
case user.UserUnlockedType:
|
||||
u.appendUnlockedEvent()
|
||||
case user.UserRemovedType:
|
||||
u.appendRemovedEvent()
|
||||
}
|
||||
|
||||
if u.Human != nil {
|
||||
u.Human.user = u
|
||||
return u.Human.AppendEvent(event)
|
||||
} else if u.Machine != nil {
|
||||
u.Machine.user = u
|
||||
return u.Machine.AppendEvent(event)
|
||||
}
|
||||
if strings.HasPrefix(string(event.Typ), "user.human") || event.AggregateVersion == "v1" {
|
||||
u.Human = &Human{user: u}
|
||||
return u.Human.AppendEvent(event)
|
||||
}
|
||||
if strings.HasPrefix(string(event.Typ), "user.machine") {
|
||||
u.Machine = &Machine{user: u}
|
||||
return u.Machine.AppendEvent(event)
|
||||
}
|
||||
|
||||
return zerrors.ThrowNotFound(nil, "MODEL-x9TaX", "Errors.UserType.Undefined")
|
||||
}
|
||||
|
||||
func (u *User) setData(event *es_models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, u); err != nil {
|
||||
logging.Log("EVEN-ZDzQy").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-yGmhh", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) appendDeactivatedEvent() {
|
||||
u.State = int32(model.UserStateInactive)
|
||||
}
|
||||
|
||||
func (u *User) appendReactivatedEvent() {
|
||||
u.State = int32(model.UserStateActive)
|
||||
}
|
||||
|
||||
func (u *User) appendLockedEvent() {
|
||||
u.State = int32(model.UserStateLocked)
|
||||
}
|
||||
|
||||
func (u *User) appendUnlockedEvent() {
|
||||
u.State = int32(model.UserStateActive)
|
||||
}
|
||||
|
||||
func (u *User) appendRemovedEvent() {
|
||||
u.State = int32(model.UserStateDeleted)
|
||||
}
|
@@ -0,0 +1,185 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Human struct {
|
||||
user *User `json:"-"`
|
||||
|
||||
*Password
|
||||
*Profile
|
||||
*Email
|
||||
*Phone
|
||||
*Address
|
||||
ExternalIDPs []*ExternalIDP `json:"-"`
|
||||
InitCode *InitUserCode `json:"-"`
|
||||
EmailCode *EmailCode `json:"-"`
|
||||
PhoneCode *PhoneCode `json:"-"`
|
||||
PasswordCode *PasswordCode `json:"-"`
|
||||
OTP *OTP `json:"-"`
|
||||
U2FTokens []*WebAuthNToken `json:"-"`
|
||||
PasswordlessTokens []*WebAuthNToken `json:"-"`
|
||||
U2FLogins []*WebAuthNLogin `json:"-"`
|
||||
PasswordlessLogins []*WebAuthNLogin `json:"-"`
|
||||
}
|
||||
|
||||
type InitUserCode struct {
|
||||
es_models.ObjectRoot
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Human) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
if err := p.AppendEvent(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Human) AppendEvent(event *es_models.Event) (err error) {
|
||||
switch event.Type() {
|
||||
case user.UserV1AddedType,
|
||||
user.UserV1RegisteredType,
|
||||
user.UserV1ProfileChangedType,
|
||||
user.HumanAddedType,
|
||||
user.HumanRegisteredType,
|
||||
user.HumanProfileChangedType:
|
||||
err = h.setData(event)
|
||||
case user.UserV1InitialCodeAddedType,
|
||||
user.HumanInitialCodeAddedType:
|
||||
err = h.appendInitUsercodeCreatedEvent(event)
|
||||
case user.UserV1PasswordChangedType,
|
||||
user.HumanPasswordChangedType:
|
||||
err = h.appendUserPasswordChangedEvent(event)
|
||||
case user.UserV1PasswordCodeAddedType,
|
||||
user.HumanPasswordCodeAddedType:
|
||||
err = h.appendPasswordSetRequestedEvent(event)
|
||||
case user.UserV1EmailChangedType,
|
||||
user.HumanEmailChangedType:
|
||||
err = h.appendUserEmailChangedEvent(event)
|
||||
case user.UserV1EmailCodeAddedType,
|
||||
user.HumanEmailCodeAddedType:
|
||||
err = h.appendUserEmailCodeAddedEvent(event)
|
||||
case user.UserV1EmailVerifiedType,
|
||||
user.HumanEmailVerifiedType:
|
||||
h.appendUserEmailVerifiedEvent()
|
||||
case user.UserV1PhoneChangedType,
|
||||
user.HumanPhoneChangedType:
|
||||
err = h.appendUserPhoneChangedEvent(event)
|
||||
case user.UserV1PhoneCodeAddedType,
|
||||
user.HumanPhoneCodeAddedType:
|
||||
err = h.appendUserPhoneCodeAddedEvent(event)
|
||||
case user.UserV1PhoneVerifiedType,
|
||||
user.HumanPhoneVerifiedType:
|
||||
h.appendUserPhoneVerifiedEvent()
|
||||
case user.UserV1PhoneRemovedType,
|
||||
user.HumanPhoneRemovedType:
|
||||
h.appendUserPhoneRemovedEvent()
|
||||
case user.UserV1AddressChangedType,
|
||||
user.HumanAddressChangedType:
|
||||
err = h.appendUserAddressChangedEvent(event)
|
||||
case user.UserV1MFAOTPAddedType,
|
||||
user.HumanMFAOTPAddedType:
|
||||
err = h.appendOTPAddedEvent(event)
|
||||
case user.UserV1MFAOTPVerifiedType,
|
||||
user.HumanMFAOTPVerifiedType:
|
||||
h.appendOTPVerifiedEvent()
|
||||
case user.UserV1MFAOTPRemovedType,
|
||||
user.HumanMFAOTPRemovedType:
|
||||
h.appendOTPRemovedEvent()
|
||||
case user.UserIDPLinkAddedType:
|
||||
err = h.appendExternalIDPAddedEvent(event)
|
||||
case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType:
|
||||
err = h.appendExternalIDPRemovedEvent(event)
|
||||
case user.HumanU2FTokenAddedType:
|
||||
err = h.appendU2FAddedEvent(event)
|
||||
case user.HumanU2FTokenVerifiedType:
|
||||
err = h.appendU2FVerifiedEvent(event)
|
||||
case user.HumanU2FTokenSignCountChangedType:
|
||||
err = h.appendU2FChangeSignCountEvent(event)
|
||||
case user.HumanU2FTokenRemovedType:
|
||||
err = h.appendU2FRemovedEvent(event)
|
||||
case user.HumanPasswordlessTokenAddedType:
|
||||
err = h.appendPasswordlessAddedEvent(event)
|
||||
case user.HumanPasswordlessTokenVerifiedType:
|
||||
err = h.appendPasswordlessVerifiedEvent(event)
|
||||
case user.HumanPasswordlessTokenSignCountChangedType:
|
||||
err = h.appendPasswordlessChangeSignCountEvent(event)
|
||||
case user.HumanPasswordlessTokenRemovedType:
|
||||
err = h.appendPasswordlessRemovedEvent(event)
|
||||
case user.HumanU2FTokenBeginLoginType:
|
||||
err = h.appendU2FLoginEvent(event)
|
||||
case user.HumanPasswordlessTokenBeginLoginType:
|
||||
err = h.appendPasswordlessLoginEvent(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.ComputeObject()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Human) ComputeObject() {
|
||||
if h.user.State == int32(model.UserStateUnspecified) || h.user.State == int32(model.UserStateInitial) {
|
||||
if h.Email != nil && h.IsEmailVerified {
|
||||
h.user.State = int32(model.UserStateActive)
|
||||
} else {
|
||||
h.user.State = int32(model.UserStateInitial)
|
||||
}
|
||||
}
|
||||
if h.Password != nil && h.Password.ObjectRoot.IsZero() {
|
||||
h.Password.ObjectRoot = h.user.ObjectRoot
|
||||
}
|
||||
if h.Profile != nil && h.Profile.ObjectRoot.IsZero() {
|
||||
h.Profile.ObjectRoot = h.user.ObjectRoot
|
||||
}
|
||||
if h.Email != nil && h.Email.ObjectRoot.IsZero() {
|
||||
h.Email.ObjectRoot = h.user.ObjectRoot
|
||||
}
|
||||
if h.Phone != nil && h.Phone.ObjectRoot.IsZero() {
|
||||
h.Phone.ObjectRoot = h.user.ObjectRoot
|
||||
}
|
||||
if h.Address != nil && h.Address.ObjectRoot.IsZero() {
|
||||
h.Address.ObjectRoot = h.user.ObjectRoot
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Human) 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 zerrors.ThrowInternal(err, "MODEL-sj4jd", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) 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 zerrors.ThrowInternal(err, "MODEL-lo34s", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
user_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Machine struct {
|
||||
user *User `json:"-"`
|
||||
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func (sa *Machine) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
if err := sa.AppendEvent(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sa *Machine) AppendEvent(event *es_models.Event) (err error) {
|
||||
switch event.Type() {
|
||||
case user_repo.MachineAddedEventType, user_repo.MachineChangedEventType:
|
||||
err = sa.setData(event)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (sa *Machine) setData(event *es_models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, sa); err != nil {
|
||||
logging.Log("EVEN-8ujgd").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-GwjY9", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MachineKey struct {
|
||||
es_models.ObjectRoot `json:"-"`
|
||||
KeyID string `json:"keyId,omitempty"`
|
||||
Type int32 `json:"type,omitempty"`
|
||||
ExpirationDate time.Time `json:"expirationDate,omitempty"`
|
||||
PublicKey []byte `json:"publicKey,omitempty"`
|
||||
privateKey []byte
|
||||
}
|
||||
|
||||
func (key *MachineKey) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
err := key.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (key *MachineKey) AppendEvent(event *es_models.Event) (err error) {
|
||||
key.ObjectRoot.AppendEvent(event)
|
||||
switch event.Type() {
|
||||
case user_repo.MachineKeyAddedEventType:
|
||||
err = json.Unmarshal(event.Data, key)
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "MODEL-SjI4S", "Errors.Internal")
|
||||
}
|
||||
case user_repo.MachineKeyRemovedEventType:
|
||||
key.ExpirationDate = event.CreationDate
|
||||
}
|
||||
return err
|
||||
}
|
@@ -0,0 +1,257 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type WebAuthNToken struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
WebauthNTokenID string `json:"webAuthNTokenId"`
|
||||
Challenge string `json:"challenge"`
|
||||
State int32 `json:"-"`
|
||||
|
||||
KeyID []byte `json:"keyId"`
|
||||
PublicKey []byte `json:"publicKey"`
|
||||
AttestationType string `json:"attestationType"`
|
||||
AAGUID []byte `json:"aaguid"`
|
||||
SignCount uint32 `json:"signCount"`
|
||||
WebAuthNTokenName string `json:"webAuthNTokenName"`
|
||||
}
|
||||
|
||||
type WebAuthNVerify struct {
|
||||
WebAuthNTokenID string `json:"webAuthNTokenId"`
|
||||
KeyID []byte `json:"keyId"`
|
||||
PublicKey []byte `json:"publicKey"`
|
||||
AttestationType string `json:"attestationType"`
|
||||
AAGUID []byte `json:"aaguid"`
|
||||
SignCount uint32 `json:"signCount"`
|
||||
WebAuthNTokenName string `json:"webAuthNTokenName"`
|
||||
UserAgentID string `json:"userAgentID,omitempty"`
|
||||
}
|
||||
|
||||
type WebAuthNSignCount struct {
|
||||
WebauthNTokenID string `json:"webAuthNTokenId"`
|
||||
SignCount uint32 `json:"signCount"`
|
||||
}
|
||||
|
||||
type WebAuthNTokenID struct {
|
||||
WebauthNTokenID string `json:"webAuthNTokenId"`
|
||||
}
|
||||
|
||||
type WebAuthNLogin struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
WebauthNTokenID string `json:"webAuthNTokenId"`
|
||||
Challenge string `json:"challenge"`
|
||||
*AuthRequest
|
||||
}
|
||||
|
||||
func GetWebauthn(webauthnTokens []*WebAuthNToken, id string) (int, *WebAuthNToken) {
|
||||
for i, webauthn := range webauthnTokens {
|
||||
if webauthn.WebauthNTokenID == id {
|
||||
return i, webauthn
|
||||
}
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (w *WebAuthNVerify) SetData(event eventstore.Event) error {
|
||||
if err := event.Unmarshal(w); err != nil {
|
||||
logging.Log("EVEN-G342rf").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-B6641", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendU2FAddedEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNToken)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
webauthn.ObjectRoot.CreationDate = event.CreatedAt()
|
||||
webauthn.State = int32(model.MFAStateNotReady)
|
||||
for i, token := range u.U2FTokens {
|
||||
if token.State == int32(model.MFAStateNotReady) {
|
||||
u.U2FTokens[i] = webauthn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
u.U2FTokens = append(u.U2FTokens, webauthn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendU2FVerifiedEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNToken)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, token := GetWebauthn(u.U2FTokens, webauthn.WebauthNTokenID); token != nil {
|
||||
err := token.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token.State = int32(model.MFAStateReady)
|
||||
return nil
|
||||
}
|
||||
return zerrors.ThrowPreconditionFailed(nil, "MODEL-4hu9s", "Errors.Users.MFA.U2F.NotExisting")
|
||||
}
|
||||
|
||||
func (u *Human) appendU2FChangeSignCountEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNToken)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, token := GetWebauthn(u.U2FTokens, webauthn.WebauthNTokenID); token != nil {
|
||||
token.setData(event)
|
||||
return nil
|
||||
}
|
||||
return zerrors.ThrowPreconditionFailed(nil, "MODEL-5Ms8h", "Errors.Users.MFA.U2F.NotExisting")
|
||||
}
|
||||
|
||||
func (u *Human) appendU2FRemovedEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNToken)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := len(u.U2FTokens) - 1; i >= 0; i-- {
|
||||
if u.U2FTokens[i].WebauthNTokenID == webauthn.WebauthNTokenID {
|
||||
copy(u.U2FTokens[i:], u.U2FTokens[i+1:])
|
||||
u.U2FTokens[len(u.U2FTokens)-1] = nil
|
||||
u.U2FTokens = u.U2FTokens[:len(u.U2FTokens)-1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendPasswordlessAddedEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNToken)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
webauthn.ObjectRoot.CreationDate = event.CreatedAt()
|
||||
webauthn.State = int32(model.MFAStateNotReady)
|
||||
for i, token := range u.PasswordlessTokens {
|
||||
if token.State == int32(model.MFAStateNotReady) {
|
||||
u.PasswordlessTokens[i] = webauthn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
u.PasswordlessTokens = append(u.PasswordlessTokens, webauthn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendPasswordlessVerifiedEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNToken)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, token := GetWebauthn(u.PasswordlessTokens, webauthn.WebauthNTokenID); token != nil {
|
||||
err := token.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token.State = int32(model.MFAStateReady)
|
||||
return nil
|
||||
}
|
||||
return zerrors.ThrowPreconditionFailed(nil, "MODEL-mKns8", "Errors.Users.MFA.Passwordless.NotExisting")
|
||||
}
|
||||
|
||||
func (u *Human) appendPasswordlessChangeSignCountEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNToken)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, token := GetWebauthn(u.PasswordlessTokens, webauthn.WebauthNTokenID); token != nil {
|
||||
err := token.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return zerrors.ThrowPreconditionFailed(nil, "MODEL-2Mv9s", "Errors.Users.MFA.Passwordless.NotExisting")
|
||||
}
|
||||
|
||||
func (u *Human) appendPasswordlessRemovedEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNToken)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := len(u.PasswordlessTokens) - 1; i >= 0; i-- {
|
||||
if u.PasswordlessTokens[i].WebauthNTokenID == webauthn.WebauthNTokenID {
|
||||
copy(u.PasswordlessTokens[i:], u.PasswordlessTokens[i+1:])
|
||||
u.PasswordlessTokens[len(u.PasswordlessTokens)-1] = nil
|
||||
u.PasswordlessTokens = u.PasswordlessTokens[:len(u.PasswordlessTokens)-1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WebAuthNToken) setData(event eventstore.Event) error {
|
||||
w.ObjectRoot.AppendEvent(event)
|
||||
if err := event.Unmarshal(w); err != nil {
|
||||
logging.Log("EVEN-4M9is").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendU2FLoginEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNLogin)
|
||||
webauthn.ObjectRoot.AppendEvent(event)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
webauthn.ObjectRoot.CreationDate = event.CreatedAt()
|
||||
for i, token := range u.U2FLogins {
|
||||
if token.AuthRequest.ID == webauthn.AuthRequest.ID {
|
||||
u.U2FLogins[i] = webauthn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
u.U2FLogins = append(u.U2FLogins, webauthn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) appendPasswordlessLoginEvent(event eventstore.Event) error {
|
||||
webauthn := new(WebAuthNLogin)
|
||||
webauthn.ObjectRoot.AppendEvent(event)
|
||||
err := webauthn.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
webauthn.ObjectRoot.CreationDate = event.CreatedAt()
|
||||
for i, token := range u.PasswordlessLogins {
|
||||
if token.AuthRequest.ID == webauthn.AuthRequest.ID {
|
||||
u.PasswordlessLogins[i] = webauthn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
u.PasswordlessLogins = append(u.PasswordlessLogins, webauthn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WebAuthNLogin) setData(event eventstore.Event) error {
|
||||
w.ObjectRoot.AppendEvent(event)
|
||||
if err := event.Unmarshal(w); err != nil {
|
||||
logging.Log("EVEN-hmSlo").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,151 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user"
|
||||
)
|
||||
|
||||
func TestAppendMFAU2FAddedEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
u2f *WebAuthNToken
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append user u2f event",
|
||||
args: args{
|
||||
user: &Human{},
|
||||
u2f: &WebAuthNToken{WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{
|
||||
U2FTokens: []*WebAuthNToken{
|
||||
{WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge", State: int32(user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.u2f != nil {
|
||||
data, _ := json.Marshal(tt.args.u2f)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendU2FAddedEvent(tt.args.event)
|
||||
if tt.args.user.U2FTokens[0].State != tt.result.U2FTokens[0].State {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].State, tt.args.user.U2FTokens[0].State)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendMFAU2FVerifyEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
u2f *WebAuthNVerify
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append u2f verify event",
|
||||
args: args{
|
||||
user: &Human{
|
||||
U2FTokens: []*WebAuthNToken{
|
||||
{WebauthNTokenID: "WebauthNTokenID", Challenge: "Challenge", State: int32(user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY)},
|
||||
},
|
||||
},
|
||||
u2f: &WebAuthNVerify{WebAuthNTokenID: "WebauthNTokenID", KeyID: []byte("KeyID"), PublicKey: []byte("PublicKey"), AttestationType: "AttestationType", AAGUID: []byte("AAGUID"), SignCount: 1},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{
|
||||
U2FTokens: []*WebAuthNToken{
|
||||
{
|
||||
WebauthNTokenID: "WebauthNTokenID",
|
||||
Challenge: "Challenge",
|
||||
State: int32(user.AuthFactorState_AUTH_FACTOR_STATE_READY),
|
||||
KeyID: []byte("KeyID"),
|
||||
PublicKey: []byte("PublicKey"),
|
||||
AttestationType: "AttestationType",
|
||||
AAGUID: []byte("AAGUID"),
|
||||
SignCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.u2f != nil {
|
||||
data, _ := json.Marshal(tt.args.u2f)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendU2FVerifiedEvent(tt.args.event)
|
||||
if tt.args.user.U2FTokens[0].State != tt.result.U2FTokens[0].State {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].State, tt.args.user.U2FTokens[0].State)
|
||||
}
|
||||
if tt.args.user.U2FTokens[0].AttestationType != tt.result.U2FTokens[0].AttestationType {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.U2FTokens[0].AttestationType, tt.args.user.U2FTokens[0].AttestationType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendMFAU2FRemoveEvent(t *testing.T) {
|
||||
type args struct {
|
||||
user *Human
|
||||
u2f *WebAuthNTokenID
|
||||
event *es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Human
|
||||
}{
|
||||
{
|
||||
name: "append u2f remove event",
|
||||
args: args{
|
||||
user: &Human{
|
||||
U2FTokens: []*WebAuthNToken{
|
||||
{
|
||||
WebauthNTokenID: "WebauthNTokenID",
|
||||
Challenge: "Challenge",
|
||||
State: int32(user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY),
|
||||
KeyID: []byte("KeyID"),
|
||||
PublicKey: []byte("PublicKey"),
|
||||
AttestationType: "AttestationType",
|
||||
AAGUID: []byte("AAGUID"),
|
||||
SignCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
u2f: &WebAuthNTokenID{WebauthNTokenID: "WebauthNTokenID"},
|
||||
event: &es_models.Event{},
|
||||
},
|
||||
result: &Human{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.u2f != nil {
|
||||
data, _ := json.Marshal(tt.args.u2f)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
tt.args.user.appendU2FRemovedEvent(tt.args.event)
|
||||
if len(tt.args.user.U2FTokens) != 0 {
|
||||
t.Errorf("got wrong result: actual: %v ", tt.result.U2FTokens)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
SELECT
|
||||
s.user_agent_id,
|
||||
s.user_id,
|
||||
s.id
|
||||
FROM auth.user_sessions s
|
||||
JOIN auth.user_sessions s2
|
||||
ON s.instance_id = s2.instance_id
|
||||
AND s.user_agent_id = s2.user_agent_id
|
||||
WHERE
|
||||
s2.id = $1
|
||||
AND s.instance_id = $2
|
||||
AND s.state = 0;
|
182
apps/api/internal/user/repository/view/model/refresh_token.go
Normal file
182
apps/api/internal/user/repository/view/model/refresh_token.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
user_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
usr_model "github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
RefreshTokenKeyTokenID = "id"
|
||||
RefreshTokenKeyUserID = "user_id"
|
||||
RefreshTokenKeyApplicationID = "application_id"
|
||||
RefreshTokenKeyUserAgentID = "user_agent_id"
|
||||
RefreshTokenKeyExpiration = "expiration"
|
||||
RefreshTokenKeyResourceOwner = "resource_owner"
|
||||
RefreshTokenKeyInstanceID = "instance_id"
|
||||
RefreshTokenKeyCreationDate = "creation_date"
|
||||
RefreshTokenKeyChangeDate = "change_date"
|
||||
RefreshTokenKeySequence = "sequence"
|
||||
RefreshTokenKeyActor = "actor"
|
||||
RefreshTokenKeyAMR = "amr"
|
||||
RefreshTokenKeyAuthTime = "auth_time"
|
||||
RefreshTokenKeyAudience = "audience"
|
||||
RefreshTokenKeyClientID = "client_id"
|
||||
RefreshTokenKeyIdleExpiration = "idle_expiration"
|
||||
RefreshTokenKeyScopes = "scopes"
|
||||
RefreshTokenKeyToken = "token"
|
||||
)
|
||||
|
||||
type RefreshTokenView struct {
|
||||
ID string `json:"tokenId" gorm:"column:id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
Token string `json:"-" gorm:"column:token"`
|
||||
UserID string `json:"-" gorm:"column:user_id"`
|
||||
ClientID string `json:"clientID" gorm:"column:client_id"`
|
||||
UserAgentID string `json:"userAgentId" gorm:"column:user_agent_id"`
|
||||
Audience database.TextArray[string] `json:"audience" gorm:"column:audience"`
|
||||
Scopes database.TextArray[string] `json:"scopes" gorm:"column:scopes"`
|
||||
AuthMethodsReferences database.TextArray[string] `json:"authMethodsReference" gorm:"column:amr"`
|
||||
AuthTime time.Time `json:"authTime" gorm:"column:auth_time"`
|
||||
IdleExpiration time.Time `json:"-" gorm:"column:idle_expiration"`
|
||||
Expiration time.Time `json:"-" gorm:"column:expiration"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
|
||||
Actor TokenActor `json:"actor" gorm:"column:actor"`
|
||||
}
|
||||
|
||||
func RefreshTokenViewsToModel(tokens []*RefreshTokenView) []*usr_model.RefreshTokenView {
|
||||
result := make([]*usr_model.RefreshTokenView, len(tokens))
|
||||
for i, g := range tokens {
|
||||
result[i] = RefreshTokenViewToModel(g)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func RefreshTokenViewToModel(token *RefreshTokenView) *usr_model.RefreshTokenView {
|
||||
return &usr_model.RefreshTokenView{
|
||||
ID: token.ID,
|
||||
CreationDate: token.CreationDate,
|
||||
ChangeDate: token.ChangeDate,
|
||||
ResourceOwner: token.ResourceOwner,
|
||||
Token: token.Token,
|
||||
UserID: token.UserID,
|
||||
ClientID: token.ClientID,
|
||||
UserAgentID: token.UserAgentID,
|
||||
Audience: token.Audience,
|
||||
Scopes: token.Scopes,
|
||||
AuthMethodsReferences: token.AuthMethodsReferences,
|
||||
AuthTime: token.AuthTime,
|
||||
IdleExpiration: token.IdleExpiration,
|
||||
Expiration: token.Expiration,
|
||||
Sequence: token.Sequence,
|
||||
Actor: token.Actor.TokenActor,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *RefreshTokenView) AppendEventIfMyRefreshToken(event eventstore.Event) (err error) {
|
||||
// in case anything needs to be change here check if the Reduce function needs the change as well
|
||||
view := new(RefreshTokenView)
|
||||
switch event.Type() {
|
||||
case user_repo.HumanRefreshTokenAddedType:
|
||||
view.setRootData(event)
|
||||
err = view.appendAddedEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case user_repo.HumanRefreshTokenRenewedType:
|
||||
view.setRootData(event)
|
||||
err = view.appendRenewedEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case user_repo.HumanRefreshTokenRemovedType,
|
||||
user_repo.UserRemovedType,
|
||||
user_repo.UserDeactivatedType,
|
||||
user_repo.UserLockedType:
|
||||
view.appendRemovedEvent(event)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if view.ID == t.ID {
|
||||
return t.AppendEvent(event)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RefreshTokenView) AppendEvent(event eventstore.Event) error {
|
||||
// in case anything needs to be change here check if the Reduce function needs the change as well
|
||||
t.ChangeDate = event.CreatedAt()
|
||||
t.Sequence = event.Sequence()
|
||||
switch event.Type() {
|
||||
case user_repo.HumanRefreshTokenAddedType:
|
||||
t.setRootData(event)
|
||||
return t.appendAddedEvent(event)
|
||||
case user_repo.HumanRefreshTokenRenewedType:
|
||||
t.setRootData(event)
|
||||
return t.appendRenewedEvent(event)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RefreshTokenView) setRootData(event eventstore.Event) {
|
||||
t.UserID = event.Aggregate().ID
|
||||
t.ResourceOwner = event.Aggregate().ResourceOwner
|
||||
t.InstanceID = event.Aggregate().InstanceID
|
||||
}
|
||||
|
||||
func (t *RefreshTokenView) appendAddedEvent(event eventstore.Event) error {
|
||||
e := new(user_repo.HumanRefreshTokenAddedEvent)
|
||||
if err := event.Unmarshal(e); err != nil {
|
||||
logging.WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-Bbr42", "could not unmarshal event")
|
||||
}
|
||||
t.ID = e.TokenID
|
||||
t.CreationDate = event.CreatedAt()
|
||||
t.AuthMethodsReferences = e.AuthMethodsReferences
|
||||
t.AuthTime = e.AuthTime
|
||||
t.Audience = e.Audience
|
||||
t.ClientID = e.ClientID
|
||||
t.Expiration = event.CreatedAt().Add(e.Expiration)
|
||||
t.IdleExpiration = event.CreatedAt().Add(e.IdleExpiration)
|
||||
t.Scopes = e.Scopes
|
||||
t.Token = e.TokenID
|
||||
t.UserAgentID = e.UserAgentID
|
||||
t.Actor = TokenActor{e.Actor}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RefreshTokenView) appendRenewedEvent(event eventstore.Event) error {
|
||||
e := new(user_repo.HumanRefreshTokenRenewedEvent)
|
||||
if err := event.Unmarshal(e); err != nil {
|
||||
logging.WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-Bbrn4", "could not unmarshal event")
|
||||
}
|
||||
t.ID = e.TokenID
|
||||
t.IdleExpiration = event.CreatedAt().Add(e.IdleExpiration)
|
||||
t.Token = e.RefreshToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RefreshTokenView) appendRemovedEvent(event eventstore.Event) {
|
||||
t.Expiration = event.CreatedAt()
|
||||
}
|
||||
|
||||
func (t *RefreshTokenView) GetRelevantEventTypes() []eventstore.EventType {
|
||||
return []eventstore.EventType{
|
||||
user_repo.HumanRefreshTokenAddedType,
|
||||
user_repo.HumanRefreshTokenRenewedType,
|
||||
user_repo.HumanRefreshTokenRemovedType,
|
||||
user_repo.UserRemovedType,
|
||||
user_repo.UserDeactivatedType,
|
||||
user_repo.UserLockedType,
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
type RefreshTokenSearchRequest model.RefreshTokenSearchRequest
|
||||
type RefreshTokenSearchQuery model.RefreshTokenSearchQuery
|
||||
type RefreshTokenSearchKey model.RefreshTokenSearchKey
|
||||
|
||||
func (req RefreshTokenSearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req RefreshTokenSearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req RefreshTokenSearchRequest) GetSortingColumn() repository.ColumnKey {
|
||||
if req.SortingColumn == model.RefreshTokenSearchKeyUnspecified {
|
||||
return nil
|
||||
}
|
||||
return RefreshTokenSearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req RefreshTokenSearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req RefreshTokenSearchRequest) GetQueries() []repository.SearchQuery {
|
||||
result := make([]repository.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = RefreshTokenSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req RefreshTokenSearchQuery) GetKey() repository.ColumnKey {
|
||||
return RefreshTokenSearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req RefreshTokenSearchQuery) GetMethod() domain.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req RefreshTokenSearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key RefreshTokenSearchKey) ToColumnName() string {
|
||||
switch model.RefreshTokenSearchKey(key) {
|
||||
case model.RefreshTokenSearchKeyRefreshTokenID:
|
||||
return RefreshTokenKeyTokenID
|
||||
case model.RefreshTokenSearchKeyUserAgentID:
|
||||
return RefreshTokenKeyUserAgentID
|
||||
case model.RefreshTokenSearchKeyUserID:
|
||||
return RefreshTokenKeyUserID
|
||||
case model.RefreshTokenSearchKeyApplicationID:
|
||||
return RefreshTokenKeyApplicationID
|
||||
case model.RefreshTokenSearchKeyExpiration:
|
||||
return RefreshTokenKeyExpiration
|
||||
case model.RefreshTokenSearchKeyResourceOwner:
|
||||
return RefreshTokenKeyResourceOwner
|
||||
case model.RefreshTokenSearchKeyInstanceID:
|
||||
return RefreshTokenKeyInstanceID
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
251
apps/api/internal/user/repository/view/model/token.go
Normal file
251
apps/api/internal/user/repository/view/model/token.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
user_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
usr_model "github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenKeyTokenID = "id"
|
||||
TokenKeyUserID = "user_id"
|
||||
TokenKeyRefreshTokenID = "refresh_token_id"
|
||||
TokenKeyApplicationID = "application_id"
|
||||
TokenKeyUserAgentID = "user_agent_id"
|
||||
TokenKeyExpiration = "expiration"
|
||||
TokenKeyResourceOwner = "resource_owner"
|
||||
TokenKeyInstanceID = "instance_id"
|
||||
TokenKeyCreationDate = "creation_date"
|
||||
TokenKeyChangeDate = "change_date"
|
||||
TokenKeySequence = "sequence"
|
||||
TokenKeyActor = "actor"
|
||||
TokenKeyID = "id"
|
||||
TokenKeyAudience = "audience"
|
||||
TokenKeyPreferredLanguage = "preferred_language"
|
||||
TokenKeyScopes = "scopes"
|
||||
TokenKeyIsPat = "is_pat"
|
||||
)
|
||||
|
||||
type TokenView struct {
|
||||
ID string `json:"tokenId" gorm:"column:id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
UserID string `json:"-" gorm:"column:user_id"`
|
||||
ApplicationID string `json:"applicationId" gorm:"column:application_id"`
|
||||
UserAgentID string `json:"userAgentId" gorm:"column:user_agent_id"`
|
||||
Audience database.TextArray[string] `json:"audience" gorm:"column:audience"`
|
||||
Scopes database.TextArray[string] `json:"scopes" gorm:"column:scopes"`
|
||||
Expiration time.Time `json:"expiration" gorm:"column:expiration"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
||||
RefreshTokenID string `json:"refreshTokenID,omitempty" gorm:"refresh_token_id"`
|
||||
IsPAT bool `json:"-" gorm:"is_pat"`
|
||||
Deactivated bool `json:"-" gorm:"-"`
|
||||
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
|
||||
Actor TokenActor `json:"actor" gorm:"column:actor"`
|
||||
}
|
||||
|
||||
type TokenActor struct {
|
||||
*domain.TokenActor
|
||||
}
|
||||
|
||||
func (a *TokenActor) Scan(value any) error {
|
||||
var data []byte
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
a.TokenActor = nil
|
||||
case string:
|
||||
data = []byte(v)
|
||||
case []byte:
|
||||
data = v
|
||||
default:
|
||||
return zerrors.ThrowInternalf(nil, "MODEL-yo8Ae", "cannot scan type %T into %T", v, a)
|
||||
}
|
||||
if err := json.Unmarshal(data, &a.TokenActor); err != nil {
|
||||
return zerrors.ThrowInternal(nil, "MODEL-yo8Ae", "cannot unmarshal token actor")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a TokenActor) Value() (driver.Value, error) {
|
||||
if a.TokenActor == nil {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := json.Marshal(a.TokenActor)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(nil, "MODEL-oD2mi", "cannot marshal token actor")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func TokenViewToModel(token *TokenView) *usr_model.TokenView {
|
||||
return &usr_model.TokenView{
|
||||
ID: token.ID,
|
||||
CreationDate: token.CreationDate,
|
||||
ChangeDate: token.ChangeDate,
|
||||
ResourceOwner: token.ResourceOwner,
|
||||
UserID: token.UserID,
|
||||
ApplicationID: token.ApplicationID,
|
||||
UserAgentID: token.UserAgentID,
|
||||
Audience: token.Audience,
|
||||
Scopes: token.Scopes,
|
||||
Expiration: token.Expiration,
|
||||
Sequence: token.Sequence,
|
||||
PreferredLanguage: token.PreferredLanguage,
|
||||
RefreshTokenID: token.RefreshTokenID,
|
||||
IsPAT: token.IsPAT,
|
||||
Actor: token.Actor.TokenActor,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TokenView) AppendEventIfMyToken(event eventstore.Event) (err error) {
|
||||
// in case anything needs to be change here check if the Reduce function needs the change as well
|
||||
view := new(TokenView)
|
||||
switch event.Type() {
|
||||
case user_repo.UserTokenAddedType,
|
||||
user_repo.PersonalAccessTokenAddedType:
|
||||
view.setRootData(event)
|
||||
err = view.setData(event)
|
||||
case user_repo.UserTokenRemovedType:
|
||||
return t.appendTokenRemoved(event)
|
||||
case user_repo.HumanRefreshTokenRemovedType:
|
||||
return t.appendRefreshTokenRemoved(event)
|
||||
case user_repo.UserV1SignedOutType,
|
||||
user_repo.HumanSignedOutType:
|
||||
id, err := UserAgentIDFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.UserAgentID == id {
|
||||
t.Deactivated = true
|
||||
}
|
||||
return nil
|
||||
case user_repo.UserRemovedType,
|
||||
user_repo.UserDeactivatedType,
|
||||
user_repo.UserLockedType:
|
||||
t.Deactivated = true
|
||||
return nil
|
||||
case user_repo.UserUnlockedType,
|
||||
user_repo.UserReactivatedType:
|
||||
if t.ID != "" && event.CreatedAt().Before(t.CreationDate) {
|
||||
t.Deactivated = false
|
||||
}
|
||||
return nil
|
||||
case user_repo.PersonalAccessTokenRemovedType:
|
||||
return t.appendPATRemoved(event)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if view.ID == t.ID {
|
||||
return t.AppendEvent(event)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TokenView) AppendEvent(event eventstore.Event) error {
|
||||
// in case anything needs to be change here check if the Reduce function needs the change as well
|
||||
t.ChangeDate = event.CreatedAt()
|
||||
t.Sequence = event.Sequence()
|
||||
switch event.Type() {
|
||||
case user_repo.UserTokenAddedType,
|
||||
user_repo.PersonalAccessTokenAddedType:
|
||||
t.setRootData(event)
|
||||
err := t.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CreationDate = event.CreatedAt()
|
||||
t.IsPAT = event.Type() == user_repo.PersonalAccessTokenAddedType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TokenView) setRootData(event eventstore.Event) {
|
||||
t.UserID = event.Aggregate().ID
|
||||
t.ResourceOwner = event.Aggregate().ResourceOwner
|
||||
t.InstanceID = event.Aggregate().InstanceID
|
||||
}
|
||||
|
||||
func (t *TokenView) setData(event eventstore.Event) error {
|
||||
if err := event.Unmarshal(t); err != nil {
|
||||
logging.WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(err, "MODEL-5Gms9", "could not unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TokenView) appendTokenRemoved(event eventstore.Event) error {
|
||||
tokenID, err := tokenIDFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tokenID == t.ID {
|
||||
t.Deactivated = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TokenView) appendRefreshTokenRemoved(event eventstore.Event) error {
|
||||
tokenID, err := tokenIDFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tokenID == t.RefreshTokenID {
|
||||
t.Deactivated = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TokenView) appendPATRemoved(event eventstore.Event) error {
|
||||
tokenID, err := tokenIDFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tokenID == t.ID && t.IsPAT {
|
||||
t.Deactivated = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TokenView) GetRelevantEventTypes() []eventstore.EventType {
|
||||
return []eventstore.EventType{
|
||||
user_repo.UserTokenAddedType,
|
||||
user_repo.PersonalAccessTokenAddedType,
|
||||
user_repo.UserTokenRemovedType,
|
||||
user_repo.HumanRefreshTokenRemovedType,
|
||||
user_repo.UserV1SignedOutType,
|
||||
user_repo.HumanSignedOutType,
|
||||
user_repo.UserRemovedType,
|
||||
user_repo.UserDeactivatedType,
|
||||
user_repo.UserLockedType,
|
||||
user_repo.UserLockedType,
|
||||
user_repo.UserReactivatedType,
|
||||
user_repo.PersonalAccessTokenRemovedType,
|
||||
}
|
||||
}
|
||||
|
||||
type tokenIDPayload struct {
|
||||
ID string `json:"tokenId"`
|
||||
}
|
||||
|
||||
func tokenIDFromEvent(event eventstore.Event) (string, error) {
|
||||
m := new(tokenIDPayload)
|
||||
if err := event.Unmarshal(&m); err != nil {
|
||||
logging.WithError(err).Error("could not unmarshal event data")
|
||||
return "", zerrors.ThrowInternal(nil, "MODEL-SDAfw", "could not unmarshal data")
|
||||
}
|
||||
return m.ID, nil
|
||||
}
|
73
apps/api/internal/user/repository/view/model/token_query.go
Normal file
73
apps/api/internal/user/repository/view/model/token_query.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
type TokenSearchRequest model.TokenSearchRequest
|
||||
type TokenSearchQuery model.TokenSearchQuery
|
||||
type TokenSearchKey model.TokenSearchKey
|
||||
|
||||
func (req TokenSearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req TokenSearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req TokenSearchRequest) GetSortingColumn() repository.ColumnKey {
|
||||
if req.SortingColumn == model.TokenSearchKeyUnspecified {
|
||||
return nil
|
||||
}
|
||||
return TokenSearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req TokenSearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req TokenSearchRequest) GetQueries() []repository.SearchQuery {
|
||||
result := make([]repository.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = TokenSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req TokenSearchQuery) GetKey() repository.ColumnKey {
|
||||
return TokenSearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req TokenSearchQuery) GetMethod() domain.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req TokenSearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key TokenSearchKey) ToColumnName() string {
|
||||
switch model.TokenSearchKey(key) {
|
||||
case model.TokenSearchKeyTokenID:
|
||||
return TokenKeyTokenID
|
||||
case model.TokenSearchKeyUserAgentID:
|
||||
return TokenKeyUserAgentID
|
||||
case model.TokenSearchKeyUserID:
|
||||
return TokenKeyUserID
|
||||
case model.TokenSearchKeyRefreshTokenID:
|
||||
return TokenKeyRefreshTokenID
|
||||
case model.TokenSearchKeyApplicationID:
|
||||
return TokenKeyApplicationID
|
||||
case model.TokenSearchKeyExpiration:
|
||||
return TokenKeyExpiration
|
||||
case model.TokenSearchKeyResourceOwner:
|
||||
return TokenKeyResourceOwner
|
||||
case model.TokenSearchKeyInstanceID:
|
||||
return TokenKeyInstanceID
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
616
apps/api/internal/user/repository/view/model/user.go
Normal file
616
apps/api/internal/user/repository/view/model/user.go
Normal file
@@ -0,0 +1,616 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
org_model "github.com/zitadel/zitadel/internal/org/model"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
UserKeyUserID = "id"
|
||||
UserKeyUserName = "user_name"
|
||||
UserKeyFirstName = "first_name"
|
||||
UserKeyLastName = "last_name"
|
||||
UserKeyNickName = "nick_name"
|
||||
UserKeyDisplayName = "display_name"
|
||||
UserKeyEmail = "email"
|
||||
UserKeyState = "user_state"
|
||||
UserKeyResourceOwner = "resource_owner"
|
||||
UserKeyLoginNames = "login_names"
|
||||
UserKeyPreferredLoginName = "preferred_login_name"
|
||||
UserKeyType = "user_type"
|
||||
UserKeyInstanceID = "instance_id"
|
||||
UserKeyOwnerRemoved = "owner_removed"
|
||||
UserKeyPasswordSet = "password_set"
|
||||
UserKeyPasswordInitRequired = "password_init_required"
|
||||
UserKeyPasswordChange = "password_change"
|
||||
UserKeyInitRequired = "init_required"
|
||||
UserKeyPasswordlessInitRequired = "passwordless_init_required"
|
||||
UserKeyMFAInitSkipped = "mfa_init_skipped"
|
||||
UserKeyChangeDate = "change_date"
|
||||
)
|
||||
|
||||
type UserView struct {
|
||||
ID string `json:"-" gorm:"column:id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
State int32 `json:"-" gorm:"column:user_state"`
|
||||
LastLogin time.Time `json:"-" gorm:"column:last_login"`
|
||||
LoginNames database.TextArray[string] `json:"-" gorm:"column:login_names"`
|
||||
PreferredLoginName string `json:"-" gorm:"column:preferred_login_name"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
UserName string `json:"userName" gorm:"column:user_name"`
|
||||
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
|
||||
*MachineView
|
||||
*HumanView
|
||||
}
|
||||
|
||||
type UserState int32
|
||||
|
||||
const (
|
||||
UserStateUnspecified UserState = iota
|
||||
UserStateActive
|
||||
UserStateInactive
|
||||
UserStateDeleted
|
||||
UserStateLocked
|
||||
UserStateSuspend
|
||||
UserStateInitial
|
||||
)
|
||||
|
||||
type HumanView struct {
|
||||
FirstName string `json:"firstName" gorm:"column:first_name"`
|
||||
LastName string `json:"lastName" gorm:"column:last_name"`
|
||||
NickName string `json:"nickName" gorm:"column:nick_name"`
|
||||
DisplayName string `json:"displayName" gorm:"column:display_name"`
|
||||
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
||||
Gender int32 `json:"gender" gorm:"column:gender"`
|
||||
AvatarKey string `json:"storeKey" gorm:"column:avatar_key"`
|
||||
Email string `json:"email" gorm:"column:email"`
|
||||
IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"`
|
||||
VerifiedEmail string `json:"-" gorm:"column:verified_email"`
|
||||
Phone string `json:"phone" gorm:"column:phone"`
|
||||
IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"`
|
||||
Country string `json:"country" gorm:"column:country"`
|
||||
Locality string `json:"locality" gorm:"column:locality"`
|
||||
PostalCode string `json:"postalCode" gorm:"column:postal_code"`
|
||||
Region string `json:"region" gorm:"column:region"`
|
||||
StreetAddress string `json:"streetAddress" gorm:"column:street_address"`
|
||||
OTPState int32 `json:"-" gorm:"column:otp_state"`
|
||||
OTPSMSAdded bool `json:"-" gorm:"column:otp_sms_added"`
|
||||
OTPEmailAdded bool `json:"-" gorm:"column:otp_email_added"`
|
||||
U2FTokens WebAuthNTokens `json:"-" gorm:"column:u2f_tokens"`
|
||||
MFAMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"`
|
||||
MFAInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"`
|
||||
InitRequired bool `json:"-" gorm:"column:init_required"`
|
||||
PasswordlessInitRequired bool `json:"-" gorm:"column:passwordless_init_required"`
|
||||
PasswordInitRequired bool `json:"-" gorm:"column:password_init_required"`
|
||||
PasswordSet bool `json:"-" gorm:"column:password_set"`
|
||||
PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"`
|
||||
UsernameChangeRequired bool `json:"-" gorm:"column:username_change_required"`
|
||||
PasswordChanged time.Time `json:"-" gorm:"column:password_change"`
|
||||
PasswordlessTokens WebAuthNTokens `json:"-" gorm:"column:passwordless_tokens"`
|
||||
}
|
||||
|
||||
type WebAuthNTokens []*WebAuthNView
|
||||
|
||||
type WebAuthNView struct {
|
||||
ID string `json:"webAuthNTokenId"`
|
||||
Name string `json:"webAuthNTokenName,omitempty"`
|
||||
State int32 `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (t WebAuthNTokens) Value() (driver.Value, error) {
|
||||
if t == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(&t)
|
||||
}
|
||||
|
||||
func (t *WebAuthNTokens) Scan(src interface{}) error {
|
||||
if b, ok := src.([]byte); ok {
|
||||
return json.Unmarshal(b, t)
|
||||
}
|
||||
if s, ok := src.(string); ok {
|
||||
return json.Unmarshal([]byte(s), t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HumanView) IsZero() bool {
|
||||
return h == nil || h.FirstName == ""
|
||||
}
|
||||
|
||||
type MachineView struct {
|
||||
Name string `json:"name" gorm:"column:machine_name"`
|
||||
Description string `json:"description" gorm:"column:machine_description"`
|
||||
}
|
||||
|
||||
func (m *MachineView) IsZero() bool {
|
||||
return m == nil || m.Name == ""
|
||||
}
|
||||
|
||||
func UserToModel(user *UserView) *model.UserView {
|
||||
userView := &model.UserView{
|
||||
ID: user.ID,
|
||||
UserName: user.UserName,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
State: model.UserState(user.State),
|
||||
LastLogin: user.LastLogin,
|
||||
PreferredLoginName: user.PreferredLoginName,
|
||||
LoginNames: user.LoginNames,
|
||||
Sequence: user.Sequence,
|
||||
}
|
||||
if !user.HumanView.IsZero() {
|
||||
userView.HumanView = &model.HumanView{
|
||||
PasswordSet: user.PasswordSet,
|
||||
PasswordInitRequired: user.PasswordInitRequired,
|
||||
PasswordChangeRequired: user.PasswordChangeRequired,
|
||||
PasswordChanged: user.PasswordChanged,
|
||||
PasswordlessTokens: WebauthnTokensToModel(user.PasswordlessTokens),
|
||||
U2FTokens: WebauthnTokensToModel(user.U2FTokens),
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
AvatarKey: user.AvatarKey,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: model.Gender(user.Gender),
|
||||
Email: user.Email,
|
||||
IsEmailVerified: user.IsEmailVerified,
|
||||
VerifiedEmail: user.VerifiedEmail,
|
||||
Phone: user.Phone,
|
||||
IsPhoneVerified: user.IsPhoneVerified,
|
||||
Country: user.Country,
|
||||
Locality: user.Locality,
|
||||
PostalCode: user.PostalCode,
|
||||
Region: user.Region,
|
||||
StreetAddress: user.StreetAddress,
|
||||
OTPState: model.MFAState(user.OTPState),
|
||||
OTPSMSAdded: user.OTPSMSAdded,
|
||||
OTPEmailAdded: user.OTPEmailAdded,
|
||||
MFAMaxSetUp: domain.MFALevel(user.MFAMaxSetUp),
|
||||
MFAInitSkipped: user.MFAInitSkipped,
|
||||
InitRequired: user.InitRequired,
|
||||
PasswordlessInitRequired: user.PasswordlessInitRequired,
|
||||
}
|
||||
}
|
||||
|
||||
if !user.MachineView.IsZero() {
|
||||
userView.MachineView = &model.MachineView{
|
||||
Description: user.MachineView.Description,
|
||||
Name: user.MachineView.Name,
|
||||
}
|
||||
}
|
||||
return userView
|
||||
}
|
||||
|
||||
func WebauthnTokensToModel(tokens []*WebAuthNView) []*model.WebAuthNView {
|
||||
if tokens == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]*model.WebAuthNView, len(tokens))
|
||||
for i, t := range tokens {
|
||||
result[i] = WebauthnTokenToModel(t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func WebauthnTokenToModel(token *WebAuthNView) *model.WebAuthNView {
|
||||
return &model.WebAuthNView{
|
||||
TokenID: token.ID,
|
||||
Name: token.Name,
|
||||
State: model.MFAState(token.State),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UserView) GenerateLoginName(domain string, appendDomain bool) string {
|
||||
if !appendDomain {
|
||||
return u.UserName
|
||||
}
|
||||
return u.UserName + "@" + domain
|
||||
}
|
||||
|
||||
func (u *UserView) SetLoginNames(userLoginMustBeDomain bool, domains []*org_model.OrgDomain) {
|
||||
u.LoginNames = make([]string, 0, len(domains))
|
||||
for _, d := range domains {
|
||||
if d.Verified {
|
||||
u.LoginNames = append(u.LoginNames, u.GenerateLoginName(d.Domain, true))
|
||||
}
|
||||
}
|
||||
if !userLoginMustBeDomain {
|
||||
u.LoginNames = append(u.LoginNames, u.GenerateLoginName(u.UserName, true))
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UserView) AppendEvent(event eventstore.Event) (err error) {
|
||||
// in case anything needs to be change here check if the Reduce function needs the change as well
|
||||
u.ChangeDate = event.CreatedAt()
|
||||
u.Sequence = event.Sequence()
|
||||
switch event.Type() {
|
||||
case user.MachineAddedEventType:
|
||||
u.CreationDate = event.CreatedAt()
|
||||
u.setRootData(event)
|
||||
err = u.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case user.UserV1AddedType,
|
||||
user.UserV1RegisteredType,
|
||||
user.HumanRegisteredType,
|
||||
user.HumanAddedType:
|
||||
u.CreationDate = event.CreatedAt()
|
||||
u.setRootData(event)
|
||||
err = u.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = u.setPasswordData(event)
|
||||
case user.UserRemovedType:
|
||||
u.State = int32(model.UserStateDeleted)
|
||||
case user.UserV1PasswordChangedType,
|
||||
user.HumanPasswordChangedType:
|
||||
err = u.setPasswordData(event)
|
||||
case user.HumanPasswordlessTokenAddedType:
|
||||
err = u.addPasswordlessToken(event)
|
||||
case user.HumanPasswordlessTokenVerifiedType:
|
||||
err = u.updatePasswordlessToken(event)
|
||||
case user.HumanPasswordlessTokenRemovedType:
|
||||
err = u.removePasswordlessToken(event)
|
||||
case user.UserV1ProfileChangedType,
|
||||
user.HumanProfileChangedType,
|
||||
user.UserV1AddressChangedType,
|
||||
user.HumanAddressChangedType,
|
||||
user.MachineChangedEventType:
|
||||
err = u.setData(event)
|
||||
case user.UserDomainClaimedType:
|
||||
if u.HumanView != nil {
|
||||
u.HumanView.UsernameChangeRequired = true
|
||||
}
|
||||
err = u.setData(event)
|
||||
case user.UserUserNameChangedType:
|
||||
if u.HumanView != nil {
|
||||
u.HumanView.UsernameChangeRequired = false
|
||||
}
|
||||
err = u.setData(event)
|
||||
case user.UserV1EmailChangedType,
|
||||
user.HumanEmailChangedType:
|
||||
u.IsEmailVerified = false
|
||||
err = u.setData(event)
|
||||
case user.UserV1EmailVerifiedType,
|
||||
user.HumanEmailVerifiedType:
|
||||
u.IsEmailVerified = true
|
||||
case user.UserV1PhoneChangedType,
|
||||
user.HumanPhoneChangedType:
|
||||
u.IsPhoneVerified = false
|
||||
err = u.setData(event)
|
||||
case user.UserV1PhoneVerifiedType,
|
||||
user.HumanPhoneVerifiedType:
|
||||
u.IsPhoneVerified = true
|
||||
case user.UserV1PhoneRemovedType,
|
||||
user.HumanPhoneRemovedType:
|
||||
u.Phone = ""
|
||||
u.IsPhoneVerified = false
|
||||
u.OTPSMSAdded = false
|
||||
u.MFAInitSkipped = time.Time{}
|
||||
case user.UserDeactivatedType:
|
||||
u.State = int32(model.UserStateInactive)
|
||||
case user.UserReactivatedType,
|
||||
user.UserUnlockedType:
|
||||
u.State = int32(model.UserStateActive)
|
||||
case user.UserLockedType:
|
||||
u.State = int32(model.UserStateLocked)
|
||||
case user.UserV1MFAOTPAddedType,
|
||||
user.HumanMFAOTPAddedType:
|
||||
if u.HumanView == nil {
|
||||
logging.WithFields("event_sequence", event.Sequence, "aggregate_id", event.Aggregate().ID, "instance", event.Aggregate().InstanceID).Warn("event is ignored because human not exists")
|
||||
return zerrors.ThrowInvalidArgument(nil, "MODEL-p2BXx", "event ignored: human not exists")
|
||||
}
|
||||
u.OTPState = int32(model.MFAStateNotReady)
|
||||
case user.UserV1MFAOTPVerifiedType,
|
||||
user.HumanMFAOTPVerifiedType:
|
||||
if u.HumanView == nil {
|
||||
logging.WithFields("event_sequence", event.Sequence, "aggregate_id", event.Aggregate().ID, "instance", event.Aggregate().InstanceID).Warn("event is ignored because human not exists")
|
||||
return zerrors.ThrowInvalidArgument(nil, "MODEL-o6Lcq", "event ignored: human not exists")
|
||||
}
|
||||
u.OTPState = int32(model.MFAStateReady)
|
||||
u.MFAInitSkipped = time.Time{}
|
||||
case user.UserV1MFAOTPRemovedType,
|
||||
user.HumanMFAOTPRemovedType:
|
||||
u.OTPState = int32(model.MFAStateUnspecified)
|
||||
case user.HumanOTPSMSAddedType:
|
||||
u.OTPSMSAdded = true
|
||||
case user.HumanOTPSMSRemovedType:
|
||||
u.OTPSMSAdded = false
|
||||
u.MFAInitSkipped = time.Time{}
|
||||
case user.HumanOTPEmailAddedType:
|
||||
u.OTPEmailAdded = true
|
||||
case user.HumanOTPEmailRemovedType:
|
||||
u.OTPEmailAdded = false
|
||||
u.MFAInitSkipped = time.Time{}
|
||||
case user.HumanU2FTokenAddedType:
|
||||
err = u.addU2FToken(event)
|
||||
case user.HumanU2FTokenVerifiedType:
|
||||
err = u.updateU2FToken(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.MFAInitSkipped = time.Time{}
|
||||
case user.HumanU2FTokenRemovedType:
|
||||
err = u.removeU2FToken(event)
|
||||
case user.UserV1MFAInitSkippedType,
|
||||
user.HumanMFAInitSkippedType:
|
||||
u.MFAInitSkipped = event.CreatedAt()
|
||||
case user.UserV1InitialCodeAddedType,
|
||||
user.HumanInitialCodeAddedType:
|
||||
u.InitRequired = true
|
||||
case user.UserV1InitializedCheckSucceededType,
|
||||
user.HumanInitializedCheckSucceededType:
|
||||
u.InitRequired = false
|
||||
case user.HumanAvatarAddedType:
|
||||
err = u.setData(event)
|
||||
case user.HumanAvatarRemovedType:
|
||||
u.AvatarKey = ""
|
||||
case user.HumanPasswordlessInitCodeAddedType,
|
||||
user.HumanPasswordlessInitCodeRequestedType:
|
||||
if u.HumanView == nil {
|
||||
logging.WithFields("event_sequence", event.Sequence, "aggregate_id", event.Aggregate().ID, "instance", event.Aggregate().InstanceID).Warn("event is ignored because human not exists")
|
||||
return zerrors.ThrowInvalidArgument(nil, "MODEL-MbyC0", "event ignored: human not exists")
|
||||
}
|
||||
if !u.PasswordSet {
|
||||
u.PasswordlessInitRequired = true
|
||||
u.PasswordInitRequired = false
|
||||
}
|
||||
}
|
||||
u.ComputeObject()
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *UserView) setRootData(event eventstore.Event) {
|
||||
u.ID = event.Aggregate().ID
|
||||
u.ResourceOwner = event.Aggregate().ResourceOwner
|
||||
u.InstanceID = event.Aggregate().InstanceID
|
||||
}
|
||||
|
||||
func (u *UserView) setData(event eventstore.Event) error {
|
||||
if err := event.Unmarshal(u); err != nil {
|
||||
logging.Log("MODEL-lso9e").WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(nil, "MODEL-8iows", "could not unmarshal data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) setPasswordData(event eventstore.Event) error {
|
||||
password := new(es_model.Password)
|
||||
if err := event.Unmarshal(password); err != nil {
|
||||
logging.WithError(err).Error("could not unmarshal event data")
|
||||
return zerrors.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
|
||||
}
|
||||
u.PasswordSet = password.Secret != nil || password.EncodedHash != ""
|
||||
u.PasswordInitRequired = !u.PasswordSet
|
||||
u.PasswordChangeRequired = password.ChangeRequired
|
||||
u.PasswordChanged = event.CreatedAt()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) addPasswordlessToken(event eventstore.Event) error {
|
||||
token, err := webAuthNViewFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, t := range u.PasswordlessTokens {
|
||||
if t.State == int32(model.MFAStateNotReady) {
|
||||
u.PasswordlessTokens[i].ID = token.ID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
token.State = int32(model.MFAStateNotReady)
|
||||
u.PasswordlessTokens = append(u.PasswordlessTokens, token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) updatePasswordlessToken(event eventstore.Event) error {
|
||||
token, err := webAuthNViewFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, t := range u.PasswordlessTokens {
|
||||
if t.ID == token.ID {
|
||||
u.PasswordlessTokens[i].Name = token.Name
|
||||
u.PasswordlessTokens[i].State = int32(model.MFAStateReady)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) removePasswordlessToken(event eventstore.Event) error {
|
||||
token, err := webAuthNViewFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, t := range u.PasswordlessTokens {
|
||||
if t.ID == token.ID {
|
||||
u.PasswordlessTokens[i] = u.PasswordlessTokens[len(u.PasswordlessTokens)-1]
|
||||
u.PasswordlessTokens[len(u.PasswordlessTokens)-1] = nil
|
||||
u.PasswordlessTokens = u.PasswordlessTokens[:len(u.PasswordlessTokens)-1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) addU2FToken(event eventstore.Event) error {
|
||||
token, err := webAuthNViewFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, t := range u.U2FTokens {
|
||||
if t.State == int32(model.MFAStateNotReady) {
|
||||
u.U2FTokens[i].ID = token.ID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
token.State = int32(model.MFAStateNotReady)
|
||||
u.U2FTokens = append(u.U2FTokens, token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) updateU2FToken(event eventstore.Event) error {
|
||||
token, err := webAuthNViewFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, t := range u.U2FTokens {
|
||||
if t.ID == token.ID {
|
||||
u.U2FTokens[i].Name = token.Name
|
||||
u.U2FTokens[i].State = int32(model.MFAStateReady)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) removeU2FToken(event eventstore.Event) error {
|
||||
token, err := webAuthNViewFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := len(u.U2FTokens) - 1; i >= 0; i-- {
|
||||
if u.U2FTokens[i].ID == token.ID {
|
||||
u.U2FTokens[i] = u.U2FTokens[len(u.U2FTokens)-1]
|
||||
u.U2FTokens[len(u.U2FTokens)-1] = nil
|
||||
u.U2FTokens = u.U2FTokens[:len(u.U2FTokens)-1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webAuthNViewFromEvent(event eventstore.Event) (*WebAuthNView, error) {
|
||||
token := new(WebAuthNView)
|
||||
err := event.Unmarshal(token)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "MODEL-FSaq1", "could not unmarshal data")
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (u *UserView) ComputeObject() {
|
||||
if !u.MachineView.IsZero() {
|
||||
if u.State == int32(model.UserStateUnspecified) {
|
||||
u.State = int32(model.UserStateActive)
|
||||
}
|
||||
return
|
||||
}
|
||||
if u.State == int32(model.UserStateUnspecified) || u.State == int32(model.UserStateInitial) {
|
||||
if u.IsEmailVerified {
|
||||
u.State = int32(model.UserStateActive)
|
||||
} else {
|
||||
u.State = int32(model.UserStateInitial)
|
||||
}
|
||||
}
|
||||
u.ComputeMFAMaxSetUp()
|
||||
}
|
||||
|
||||
func (u *UserView) ComputeMFAMaxSetUp() {
|
||||
for _, token := range u.PasswordlessTokens {
|
||||
if token.State == int32(model.MFAStateReady) {
|
||||
u.MFAMaxSetUp = int32(domain.MFALevelMultiFactor)
|
||||
u.PasswordlessInitRequired = false
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, token := range u.U2FTokens {
|
||||
if token.State == int32(model.MFAStateReady) {
|
||||
u.MFAMaxSetUp = int32(domain.MFALevelSecondFactor)
|
||||
return
|
||||
}
|
||||
}
|
||||
if u.OTPState == int32(model.MFAStateReady) ||
|
||||
u.OTPSMSAdded || u.OTPEmailAdded {
|
||||
u.MFAMaxSetUp = int32(domain.MFALevelSecondFactor)
|
||||
return
|
||||
}
|
||||
u.MFAMaxSetUp = int32(domain.MFALevelNotSetUp)
|
||||
}
|
||||
|
||||
func (u *UserView) SetEmptyUserType() {
|
||||
if u.MachineView != nil && u.MachineView.Name == "" {
|
||||
u.MachineView = nil
|
||||
} else {
|
||||
u.HumanView = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UserView) EventTypes() []eventstore.EventType {
|
||||
return []eventstore.EventType{
|
||||
user.MachineAddedEventType,
|
||||
user.UserV1AddedType,
|
||||
user.UserV1RegisteredType,
|
||||
user.HumanRegisteredType,
|
||||
user.HumanAddedType,
|
||||
user.UserRemovedType,
|
||||
user.UserV1PasswordChangedType,
|
||||
user.HumanPasswordChangedType,
|
||||
user.HumanPasswordlessTokenAddedType,
|
||||
user.HumanPasswordlessTokenVerifiedType,
|
||||
user.HumanPasswordlessTokenRemovedType,
|
||||
user.UserV1ProfileChangedType,
|
||||
user.HumanProfileChangedType,
|
||||
user.UserV1AddressChangedType,
|
||||
user.HumanAddressChangedType,
|
||||
user.MachineChangedEventType,
|
||||
user.UserDomainClaimedType,
|
||||
user.UserUserNameChangedType,
|
||||
user.UserV1EmailChangedType,
|
||||
user.HumanEmailChangedType,
|
||||
user.UserV1EmailVerifiedType,
|
||||
user.HumanEmailVerifiedType,
|
||||
user.UserV1PhoneChangedType,
|
||||
user.HumanPhoneChangedType,
|
||||
user.UserV1PhoneVerifiedType,
|
||||
user.HumanPhoneVerifiedType,
|
||||
user.UserV1PhoneRemovedType,
|
||||
user.HumanPhoneRemovedType,
|
||||
user.UserDeactivatedType,
|
||||
user.UserReactivatedType,
|
||||
user.UserUnlockedType,
|
||||
user.UserLockedType,
|
||||
user.UserV1MFAOTPAddedType,
|
||||
user.HumanMFAOTPAddedType,
|
||||
user.UserV1MFAOTPVerifiedType,
|
||||
user.HumanMFAOTPVerifiedType,
|
||||
user.UserV1MFAOTPRemovedType,
|
||||
user.HumanMFAOTPRemovedType,
|
||||
user.HumanOTPSMSAddedType,
|
||||
user.HumanOTPSMSRemovedType,
|
||||
user.HumanOTPEmailAddedType,
|
||||
user.HumanOTPEmailRemovedType,
|
||||
user.HumanU2FTokenAddedType,
|
||||
user.HumanU2FTokenVerifiedType,
|
||||
user.HumanU2FTokenRemovedType,
|
||||
user.UserV1MFAInitSkippedType,
|
||||
user.HumanMFAInitSkippedType,
|
||||
user.UserV1InitialCodeAddedType,
|
||||
user.HumanInitialCodeAddedType,
|
||||
user.UserV1InitializedCheckSucceededType,
|
||||
user.HumanInitializedCheckSucceededType,
|
||||
user.HumanAvatarAddedType,
|
||||
user.HumanAvatarRemovedType,
|
||||
user.HumanPasswordlessInitCodeAddedType,
|
||||
user.HumanPasswordlessInitCodeRequestedType,
|
||||
}
|
||||
}
|
85
apps/api/internal/user/repository/view/model/user_query.go
Normal file
85
apps/api/internal/user/repository/view/model/user_query.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
usr_model "github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
type UserSearchRequest usr_model.UserSearchRequest
|
||||
type UserSearchQuery usr_model.UserSearchQuery
|
||||
type UserSearchKey usr_model.UserSearchKey
|
||||
|
||||
func (req UserSearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req UserSearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req UserSearchRequest) GetSortingColumn() repository.ColumnKey {
|
||||
if req.SortingColumn == usr_model.UserSearchKeyUnspecified {
|
||||
return nil
|
||||
}
|
||||
return UserSearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req UserSearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req UserSearchRequest) GetQueries() []repository.SearchQuery {
|
||||
result := make([]repository.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = UserSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req UserSearchQuery) GetKey() repository.ColumnKey {
|
||||
return UserSearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req UserSearchQuery) GetMethod() domain.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req UserSearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key UserSearchKey) ToColumnName() string {
|
||||
switch usr_model.UserSearchKey(key) {
|
||||
case usr_model.UserSearchKeyUserID:
|
||||
return UserKeyUserID
|
||||
case usr_model.UserSearchKeyUserName:
|
||||
return UserKeyUserName
|
||||
case usr_model.UserSearchKeyFirstName:
|
||||
return UserKeyFirstName
|
||||
case usr_model.UserSearchKeyLastName:
|
||||
return UserKeyLastName
|
||||
case usr_model.UserSearchKeyDisplayName:
|
||||
return UserKeyDisplayName
|
||||
case usr_model.UserSearchKeyNickName:
|
||||
return UserKeyNickName
|
||||
case usr_model.UserSearchKeyEmail:
|
||||
return UserKeyEmail
|
||||
case usr_model.UserSearchKeyState:
|
||||
return UserKeyState
|
||||
case usr_model.UserSearchKeyResourceOwner:
|
||||
return UserKeyResourceOwner
|
||||
case usr_model.UserSearchKeyLoginNames:
|
||||
return UserKeyLoginNames
|
||||
case usr_model.UserSearchKeyPreferredLoginName:
|
||||
return UserKeyPreferredLoginName
|
||||
case usr_model.UserSearchKeyType:
|
||||
return UserKeyType
|
||||
case usr_model.UserSearchKeyInstanceID:
|
||||
return UserKeyInstanceID
|
||||
case usr_model.UserSearchOwnerRemoved:
|
||||
return UserKeyOwnerRemoved
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
267
apps/api/internal/user/repository/view/model/user_session.go
Normal file
267
apps/api/internal/user/repository/view/model/user_session.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
UserSessionKeyUserAgentID = "user_agent_id"
|
||||
UserSessionKeyUserID = "user_id"
|
||||
UserSessionKeyState = "state"
|
||||
UserSessionKeyResourceOwner = "resource_owner"
|
||||
UserSessionKeyInstanceID = "instance_id"
|
||||
UserSessionKeyOwnerRemoved = "owner_removed"
|
||||
UserSessionKeyCreationDate = "creation_date"
|
||||
UserSessionKeyChangeDate = "change_date"
|
||||
UserSessionKeySequence = "sequence"
|
||||
UserSessionKeyPasswordVerification = "password_verification"
|
||||
UserSessionKeySecondFactorVerification = "second_factor_verification"
|
||||
UserSessionKeySecondFactorVerificationType = "second_factor_verification_type"
|
||||
UserSessionKeyMultiFactorVerification = "multi_factor_verification"
|
||||
UserSessionKeyMultiFactorVerificationType = "multi_factor_verification_type"
|
||||
UserSessionKeyPasswordlessVerification = "passwordless_verification"
|
||||
UserSessionKeyExternalLoginVerification = "external_login_verification"
|
||||
UserSessionKeySelectedIDPConfigID = "selected_idp_config_id"
|
||||
UserSessionKeyID = "id"
|
||||
)
|
||||
|
||||
type UserSessionView struct {
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
State sql.Null[domain.UserSessionState] `json:"-" gorm:"column:state"`
|
||||
UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id;primary_key"`
|
||||
UserID string `json:"userID" gorm:"column:user_id;primary_key"`
|
||||
// As of https://github.com/zitadel/zitadel/pull/7199 the following 4 attributes
|
||||
// are not projected in the user session handler anymore
|
||||
// and are therefore annotated with a `gorm:"-"`.
|
||||
// They will be read from the corresponding projection directly.
|
||||
UserName sql.NullString `json:"-" gorm:"-"`
|
||||
LoginName sql.NullString `json:"-" gorm:"-"`
|
||||
DisplayName sql.NullString `json:"-" gorm:"-"`
|
||||
AvatarKey sql.NullString `json:"-" gorm:"-"`
|
||||
SelectedIDPConfigID sql.NullString `json:"selectedIDPConfigID" gorm:"column:selected_idp_config_id"`
|
||||
PasswordVerification sql.NullTime `json:"-" gorm:"column:password_verification"`
|
||||
PasswordlessVerification sql.NullTime `json:"-" gorm:"column:passwordless_verification"`
|
||||
ExternalLoginVerification sql.NullTime `json:"-" gorm:"column:external_login_verification"`
|
||||
SecondFactorVerification sql.NullTime `json:"-" gorm:"column:second_factor_verification"`
|
||||
SecondFactorVerificationType sql.NullInt32 `json:"-" gorm:"column:second_factor_verification_type"`
|
||||
MultiFactorVerification sql.NullTime `json:"-" gorm:"column:multi_factor_verification"`
|
||||
MultiFactorVerificationType sql.NullInt32 `json:"-" gorm:"column:multi_factor_verification_type"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
|
||||
ID sql.NullString `json:"id" gorm:"-"`
|
||||
}
|
||||
|
||||
type ActiveUserAgentUserIDs struct {
|
||||
UserAgentID string
|
||||
UserIDs []string
|
||||
}
|
||||
|
||||
type userAgentIDPayload struct {
|
||||
ID string `json:"userAgentID"`
|
||||
}
|
||||
|
||||
func UserAgentIDFromEvent(event eventstore.Event) (string, error) {
|
||||
payload := new(userAgentIDPayload)
|
||||
if err := event.Unmarshal(payload); err != nil {
|
||||
logging.WithError(err).Error("could not unmarshal event data")
|
||||
return "", zerrors.ThrowInternal(nil, "MODEL-HJwk9", "could not unmarshal data")
|
||||
}
|
||||
return payload.ID, nil
|
||||
}
|
||||
|
||||
func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
|
||||
return &model.UserSessionView{
|
||||
ChangeDate: userSession.ChangeDate,
|
||||
CreationDate: userSession.CreationDate,
|
||||
ResourceOwner: userSession.ResourceOwner,
|
||||
State: userSession.State.V,
|
||||
UserAgentID: userSession.UserAgentID,
|
||||
UserID: userSession.UserID,
|
||||
UserName: userSession.UserName.String,
|
||||
LoginName: userSession.LoginName.String,
|
||||
DisplayName: userSession.DisplayName.String,
|
||||
AvatarKey: userSession.AvatarKey.String,
|
||||
SelectedIDPConfigID: userSession.SelectedIDPConfigID.String,
|
||||
PasswordVerification: userSession.PasswordVerification.Time,
|
||||
PasswordlessVerification: userSession.PasswordlessVerification.Time,
|
||||
ExternalLoginVerification: userSession.ExternalLoginVerification.Time,
|
||||
SecondFactorVerification: userSession.SecondFactorVerification.Time,
|
||||
SecondFactorVerificationType: domain.MFAType(userSession.SecondFactorVerificationType.Int32),
|
||||
MultiFactorVerification: userSession.MultiFactorVerification.Time,
|
||||
MultiFactorVerificationType: domain.MFAType(userSession.MultiFactorVerificationType.Int32),
|
||||
Sequence: userSession.Sequence,
|
||||
ID: userSession.ID.String,
|
||||
}
|
||||
}
|
||||
|
||||
func UserSessionsToModel(userSessions []*UserSessionView) []*model.UserSessionView {
|
||||
result := make([]*model.UserSessionView, len(userSessions))
|
||||
for i, s := range userSessions {
|
||||
result[i] = UserSessionToModel(s)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *UserSessionView) AppendEvent(event eventstore.Event) error {
|
||||
// in case anything needs to be change here check if the Reduce function needs the change as well
|
||||
v.Sequence = event.Sequence()
|
||||
v.ChangeDate = event.CreatedAt()
|
||||
switch event.Type() {
|
||||
case user.UserV1PasswordCheckSucceededType,
|
||||
user.HumanPasswordCheckSucceededType:
|
||||
v.PasswordVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
|
||||
v.State.V = domain.UserSessionStateActive
|
||||
case user.UserIDPLoginCheckSucceededType:
|
||||
data := new(es_model.AuthRequest)
|
||||
err := data.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.ExternalLoginVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
|
||||
v.SelectedIDPConfigID = sql.NullString{String: data.SelectedIDPConfigID, Valid: true}
|
||||
v.State.V = domain.UserSessionStateActive
|
||||
case user.HumanPasswordlessTokenCheckSucceededType:
|
||||
v.PasswordlessVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
|
||||
v.MultiFactorVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
|
||||
v.MultiFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFATypeU2FUserVerification)}
|
||||
v.State.V = domain.UserSessionStateActive
|
||||
case user.HumanPasswordlessTokenCheckFailedType,
|
||||
user.HumanPasswordlessTokenRemovedType:
|
||||
v.PasswordlessVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
v.MultiFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
case user.UserV1PasswordCheckFailedType,
|
||||
user.HumanPasswordCheckFailedType:
|
||||
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
case user.UserV1PasswordChangedType,
|
||||
user.HumanPasswordChangedType:
|
||||
data := new(es_model.PasswordChange)
|
||||
err := data.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v.UserAgentID != data.UserAgentID {
|
||||
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
}
|
||||
case user.HumanMFAOTPVerifiedType:
|
||||
data := new(es_model.OTPVerified)
|
||||
err := data.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v.UserAgentID == data.UserAgentID {
|
||||
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeTOTP)
|
||||
}
|
||||
case user.UserV1MFAOTPCheckSucceededType,
|
||||
user.HumanMFAOTPCheckSucceededType:
|
||||
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeTOTP)
|
||||
case user.HumanOTPSMSCheckSucceededType:
|
||||
data := new(es_model.OTPVerified)
|
||||
err := data.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v.UserAgentID == data.UserAgentID {
|
||||
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeOTPSMS)
|
||||
}
|
||||
case user.HumanOTPEmailCheckSucceededType:
|
||||
data := new(es_model.OTPVerified)
|
||||
err := data.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v.UserAgentID == data.UserAgentID {
|
||||
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeOTPEmail)
|
||||
}
|
||||
case user.UserV1MFAOTPCheckFailedType,
|
||||
user.UserV1MFAOTPRemovedType,
|
||||
user.HumanMFAOTPCheckFailedType,
|
||||
user.HumanMFAOTPRemovedType,
|
||||
user.HumanU2FTokenCheckFailedType,
|
||||
user.HumanU2FTokenRemovedType,
|
||||
user.HumanOTPSMSCheckFailedType,
|
||||
user.HumanOTPEmailCheckFailedType:
|
||||
v.SecondFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
case user.HumanU2FTokenVerifiedType:
|
||||
data := new(es_model.WebAuthNVerify)
|
||||
err := data.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v.UserAgentID == data.UserAgentID {
|
||||
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeU2F)
|
||||
}
|
||||
case user.HumanU2FTokenCheckSucceededType:
|
||||
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeU2F)
|
||||
case user.UserV1SignedOutType,
|
||||
user.HumanSignedOutType,
|
||||
user.UserLockedType,
|
||||
user.UserDeactivatedType,
|
||||
user.UserRemovedType:
|
||||
v.PasswordlessVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
v.SecondFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
v.SecondFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFALevelNotSetUp)}
|
||||
v.MultiFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
v.MultiFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFALevelNotSetUp)}
|
||||
v.ExternalLoginVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
v.State.V = domain.UserSessionStateTerminated
|
||||
case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType:
|
||||
v.ExternalLoginVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
||||
v.SelectedIDPConfigID = sql.NullString{String: "", Valid: true}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *UserSessionView) setSecondFactorVerification(verificationTime time.Time, mfaType domain.MFAType) {
|
||||
v.SecondFactorVerification = sql.NullTime{Time: verificationTime, Valid: true}
|
||||
v.SecondFactorVerificationType = sql.NullInt32{Int32: int32(mfaType)}
|
||||
v.State.V = domain.UserSessionStateActive
|
||||
}
|
||||
|
||||
func (v *UserSessionView) EventTypes() []eventstore.EventType {
|
||||
return []eventstore.EventType{
|
||||
user.UserV1PasswordCheckSucceededType,
|
||||
user.HumanPasswordCheckSucceededType,
|
||||
user.UserIDPLoginCheckSucceededType,
|
||||
user.HumanPasswordlessTokenCheckSucceededType,
|
||||
user.HumanPasswordlessTokenCheckFailedType,
|
||||
user.HumanPasswordlessTokenRemovedType,
|
||||
user.UserV1PasswordCheckFailedType,
|
||||
user.HumanPasswordCheckFailedType,
|
||||
user.UserV1PasswordChangedType,
|
||||
user.HumanPasswordChangedType,
|
||||
user.HumanMFAOTPVerifiedType,
|
||||
user.UserV1MFAOTPCheckSucceededType,
|
||||
user.HumanMFAOTPCheckSucceededType,
|
||||
user.UserV1MFAOTPCheckFailedType,
|
||||
user.UserV1MFAOTPRemovedType,
|
||||
user.HumanMFAOTPCheckFailedType,
|
||||
user.HumanMFAOTPRemovedType,
|
||||
user.HumanOTPSMSCheckSucceededType,
|
||||
user.HumanOTPSMSCheckFailedType,
|
||||
user.HumanOTPEmailCheckSucceededType,
|
||||
user.HumanOTPEmailCheckFailedType,
|
||||
user.HumanU2FTokenCheckFailedType,
|
||||
user.HumanU2FTokenRemovedType,
|
||||
user.HumanU2FTokenVerifiedType,
|
||||
user.HumanU2FTokenCheckSucceededType,
|
||||
user.UserV1SignedOutType,
|
||||
user.HumanSignedOutType,
|
||||
user.UserLockedType,
|
||||
user.UserDeactivatedType,
|
||||
user.UserIDPLinkRemovedType,
|
||||
user.UserIDPLinkCascadeRemovedType,
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
usr_model "github.com/zitadel/zitadel/internal/user/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
type UserSessionSearchRequest usr_model.UserSessionSearchRequest
|
||||
type UserSessionSearchQuery usr_model.UserSessionSearchQuery
|
||||
type UserSessionSearchKey usr_model.UserSessionSearchKey
|
||||
|
||||
func (req UserSessionSearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req UserSessionSearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req UserSessionSearchRequest) GetSortingColumn() repository.ColumnKey {
|
||||
if req.SortingColumn == usr_model.UserSessionSearchKeyUnspecified {
|
||||
return nil
|
||||
}
|
||||
return UserSessionSearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req UserSessionSearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req UserSessionSearchRequest) GetQueries() []repository.SearchQuery {
|
||||
result := make([]repository.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = UserSessionSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req UserSessionSearchQuery) GetKey() repository.ColumnKey {
|
||||
return UserSessionSearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req UserSessionSearchQuery) GetMethod() domain.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req UserSessionSearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key UserSessionSearchKey) ToColumnName() string {
|
||||
switch usr_model.UserSessionSearchKey(key) {
|
||||
case usr_model.UserSessionSearchKeyUserAgentID:
|
||||
return UserSessionKeyUserAgentID
|
||||
case usr_model.UserSessionSearchKeyUserID:
|
||||
return UserSessionKeyUserID
|
||||
case usr_model.UserSessionSearchKeyState:
|
||||
return UserSessionKeyState
|
||||
case usr_model.UserSessionSearchKeyResourceOwner:
|
||||
return UserSessionKeyResourceOwner
|
||||
case usr_model.UserSessionSearchKeyInstanceID:
|
||||
return UserSessionKeyInstanceID
|
||||
case usr_model.UserSessionSearchKeyOwnerRemoved:
|
||||
return UserSessionKeyOwnerRemoved
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
@@ -0,0 +1,242 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
func now() time.Time {
|
||||
return time.Now().UTC().Round(1 * time.Second)
|
||||
}
|
||||
|
||||
func TestAppendEvent(t *testing.T) {
|
||||
type args struct {
|
||||
event *es_models.Event
|
||||
userView *UserSessionView
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *UserSessionView
|
||||
}{
|
||||
{
|
||||
name: "append user password check succeeded event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1PasswordCheckSucceededType},
|
||||
userView: &UserSessionView{},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append human password check succeeded event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.HumanPasswordCheckSucceededType},
|
||||
userView: &UserSessionView{},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append user password check failed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1PasswordCheckFailedType},
|
||||
userView: &UserSessionView{PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append human password check failed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.HumanPasswordCheckFailedType},
|
||||
userView: &UserSessionView{PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append user password changed event",
|
||||
args: args{
|
||||
event: &es_models.Event{
|
||||
CreationDate: now(),
|
||||
Typ: user.UserV1PasswordChangedType,
|
||||
Data: func() []byte {
|
||||
d, _ := json.Marshal(&es_model.Password{
|
||||
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
|
||||
})
|
||||
return d
|
||||
}(),
|
||||
},
|
||||
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append human password changed event",
|
||||
args: args{
|
||||
event: &es_models.Event{
|
||||
CreationDate: now(),
|
||||
Typ: user.HumanPasswordChangedType,
|
||||
Data: func() []byte {
|
||||
d, _ := json.Marshal(&es_model.PasswordChange{
|
||||
Password: es_model.Password{
|
||||
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
|
||||
},
|
||||
})
|
||||
return d
|
||||
}(),
|
||||
},
|
||||
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append human password changed event same user agent",
|
||||
args: args{
|
||||
event: &es_models.Event{
|
||||
CreationDate: now(),
|
||||
Typ: user.HumanPasswordChangedType,
|
||||
Data: func() []byte {
|
||||
d, _ := json.Marshal(&es_model.PasswordChange{
|
||||
Password: es_model.Password{
|
||||
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
|
||||
},
|
||||
UserAgentID: "id",
|
||||
})
|
||||
return d
|
||||
}(),
|
||||
},
|
||||
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append user otp verified event",
|
||||
args: args{
|
||||
event: &es_models.Event{
|
||||
CreationDate: now(),
|
||||
Typ: user.HumanMFAOTPVerifiedType,
|
||||
Data: nil,
|
||||
},
|
||||
userView: &UserSessionView{UserAgentID: "id"},
|
||||
},
|
||||
result: &UserSessionView{UserAgentID: "id", ChangeDate: now()},
|
||||
},
|
||||
{
|
||||
name: "append user otp verified event same user agent",
|
||||
args: args{
|
||||
event: &es_models.Event{
|
||||
CreationDate: now(),
|
||||
Typ: user.HumanMFAOTPVerifiedType,
|
||||
Data: func() []byte {
|
||||
d, _ := json.Marshal(&es_model.OTPVerified{
|
||||
UserAgentID: "id",
|
||||
})
|
||||
return d
|
||||
}(),
|
||||
},
|
||||
userView: &UserSessionView{UserAgentID: "id"},
|
||||
},
|
||||
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append user otp check succeeded event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1MFAOTPCheckSucceededType},
|
||||
userView: &UserSessionView{},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append human otp check succeeded event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.HumanMFAOTPCheckSucceededType},
|
||||
userView: &UserSessionView{},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append user otp check failed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1MFAOTPCheckFailedType},
|
||||
userView: &UserSessionView{SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append human otp check failed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.HumanMFAOTPCheckFailedType},
|
||||
userView: &UserSessionView{SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append user otp removed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1MFAOTPRemovedType},
|
||||
userView: &UserSessionView{SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append human otp removed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.HumanMFAOTPRemovedType},
|
||||
userView: &UserSessionView{SecondFactorVerification: sql.NullTime{Time: now(), Valid: true}},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true}},
|
||||
},
|
||||
{
|
||||
name: "append user signed out event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.UserV1SignedOutType},
|
||||
userView: &UserSessionView{
|
||||
PasswordVerification: sql.NullTime{Time: now(), Valid: true},
|
||||
SecondFactorVerification: sql.NullTime{Time: now(), Valid: true},
|
||||
},
|
||||
},
|
||||
result: &UserSessionView{
|
||||
ChangeDate: now(),
|
||||
PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
ExternalLoginVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
PasswordlessVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
MultiFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
State: sql.Null[domain.UserSessionState]{V: domain.UserSessionStateTerminated},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append human signed out event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Typ: user.HumanSignedOutType},
|
||||
userView: &UserSessionView{
|
||||
PasswordVerification: sql.NullTime{Time: now(), Valid: true},
|
||||
SecondFactorVerification: sql.NullTime{Time: now(), Valid: true},
|
||||
},
|
||||
},
|
||||
result: &UserSessionView{
|
||||
ChangeDate: now(),
|
||||
PasswordVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
SecondFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
ExternalLoginVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
PasswordlessVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
MultiFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
|
||||
State: sql.Null[domain.UserSessionState]{V: domain.UserSessionStateTerminated},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.userView.AppendEvent(tt.args.event)
|
||||
assert.Equal(t, tt.result, tt.args.userView)
|
||||
})
|
||||
}
|
||||
}
|
418
apps/api/internal/user/repository/view/model/user_test.go
Normal file
418
apps/api/internal/user/repository/view/model/user_test.go
Normal file
@@ -0,0 +1,418 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
func mockUserData(user *es_model.User) []byte {
|
||||
data, _ := json.Marshal(user)
|
||||
return data
|
||||
}
|
||||
|
||||
func mockPasswordData(password *es_model.Password) []byte {
|
||||
data, _ := json.Marshal(password)
|
||||
return data
|
||||
}
|
||||
|
||||
func mockProfileData(profile *es_model.Profile) []byte {
|
||||
data, _ := json.Marshal(profile)
|
||||
return data
|
||||
}
|
||||
|
||||
func mockEmailData(email *es_model.Email) []byte {
|
||||
data, _ := json.Marshal(email)
|
||||
return data
|
||||
}
|
||||
|
||||
func mockPhoneData(phone *es_model.Phone) []byte {
|
||||
data, _ := json.Marshal(phone)
|
||||
return data
|
||||
}
|
||||
|
||||
func mockAddressData(address *es_model.Address) []byte {
|
||||
data, _ := json.Marshal(address)
|
||||
return data
|
||||
}
|
||||
|
||||
func getFullHuman(password *es_model.Password) *es_model.User {
|
||||
return &es_model.User{
|
||||
UserName: "UserName",
|
||||
Human: &es_model.Human{
|
||||
Profile: &es_model.Profile{
|
||||
FirstName: "FirstName",
|
||||
LastName: "LastName",
|
||||
},
|
||||
Email: &es_model.Email{
|
||||
EmailAddress: "Email",
|
||||
},
|
||||
Phone: &es_model.Phone{
|
||||
PhoneNumber: "Phone",
|
||||
},
|
||||
Address: &es_model.Address{
|
||||
Country: "Country",
|
||||
},
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getFullMachine() *es_model.User {
|
||||
return &es_model.User{
|
||||
UserName: "UserName",
|
||||
Machine: &es_model.Machine{
|
||||
Description: "Description",
|
||||
Name: "Machine",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAppendEvent(t *testing.T) {
|
||||
type args struct {
|
||||
event eventstore.Event
|
||||
user *UserView
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *UserView
|
||||
}{
|
||||
{
|
||||
name: "append added user event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1AddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(nil))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
{
|
||||
name: "append added human event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanAddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(nil))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
{
|
||||
name: "append added machine event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.MachineAddedEventType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullMachine())},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", MachineView: &MachineView{Description: "Description", Name: "Machine"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append added user with password event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1AddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(&es_model.Password{Secret: &crypto.CryptoValue{}}))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", PasswordSet: true}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
{
|
||||
name: "append added human with password event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanAddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(&es_model.Password{Secret: &crypto.CryptoValue{}}))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", PasswordSet: true}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
{
|
||||
name: "append added user with password but change required event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1AddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}}))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", PasswordSet: true, PasswordChangeRequired: true}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
{
|
||||
name: "append added human with password but change required event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanAddedType, ResourceOwner: "GrantedOrgID", Data: mockUserData(getFullHuman(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}}))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", PasswordSet: true, PasswordChangeRequired: true}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
{
|
||||
name: "append password change event on user",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1PasswordChangedType, ResourceOwner: "GrantedOrgID", Data: mockPasswordData(&es_model.Password{Secret: &crypto.CryptoValue{}})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", PasswordSet: true}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append password change event on human",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanPasswordChangedType, ResourceOwner: "GrantedOrgID", Data: mockPasswordData(&es_model.Password{Secret: &crypto.CryptoValue{}})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", PasswordSet: true}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append password change with change required event on user",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1PasswordChangedType, ResourceOwner: "GrantedOrgID", Data: mockPasswordData(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", PasswordSet: true, PasswordChangeRequired: true}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append password change with change required event on human",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanPasswordChangedType, ResourceOwner: "GrantedOrgID", Data: mockPasswordData(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", PasswordSet: true, PasswordChangeRequired: true}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append change user profile event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1ProfileChangedType, ResourceOwner: "GrantedOrgID", Data: mockProfileData(&es_model.Profile{FirstName: "FirstNameChanged"})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstNameChanged", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
{
|
||||
name: "append change human profile event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanProfileChangedType, ResourceOwner: "GrantedOrgID", Data: mockProfileData(&es_model.Profile{FirstName: "FirstNameChanged"})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstNameChanged", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
{
|
||||
name: "append change user email event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1EmailChangedType, ResourceOwner: "GrantedOrgID", Data: mockEmailData(&es_model.Email{EmailAddress: "EmailChanged"})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "EmailChanged", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append change human email event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanEmailChangedType, ResourceOwner: "GrantedOrgID", Data: mockEmailData(&es_model.Email{EmailAddress: "EmailChanged"})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "EmailChanged", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append verify user email event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1EmailVerifiedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append verify human email event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanEmailVerifiedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInitial)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append change user phone event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1PhoneChangedType, ResourceOwner: "GrantedOrgID", Data: mockPhoneData(&es_model.Phone{PhoneNumber: "PhoneChanged"})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "PhoneChanged", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append change human phone event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanPhoneChangedType, ResourceOwner: "GrantedOrgID", Data: mockPhoneData(&es_model.Phone{PhoneNumber: "PhoneChanged"})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "PhoneChanged", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append verify user phone event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1PhoneVerifiedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", IsPhoneVerified: true, Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append verify human phone event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanPhoneVerifiedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", IsPhoneVerified: true, Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append change user address event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1AddressChangedType, ResourceOwner: "GrantedOrgID", Data: mockAddressData(&es_model.Address{Country: "CountryChanged"})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "CountryChanged"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append change human address event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanAddressChangedType, ResourceOwner: "GrantedOrgID", Data: mockAddressData(&es_model.Address{Country: "CountryChanged"})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "CountryChanged"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append user deactivate event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserDeactivatedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInactive)},
|
||||
},
|
||||
{
|
||||
name: "append user reactivate event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserReactivatedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateInactive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append user lock event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserLockedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateLocked)},
|
||||
},
|
||||
{
|
||||
name: "append user unlock event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserUnlockedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateLocked)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append user add otp event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1MFAOTPAddedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append human add otp event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanMFAOTPAddedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append user verify otp event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1MFAOTPVerifiedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append human verify otp event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanMFAOTPVerifiedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateNotReady)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append user remove otp event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.UserV1MFAOTPRemovedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateUnspecified)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append human remove otp event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Seq: 1, Typ: user.HumanMFAOTPRemovedType, ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateReady)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", OTPState: int32(model.MFAStateUnspecified)}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append user mfa init skipped event",
|
||||
args: args{
|
||||
event: &es_models.Event{Seq: 1, CreationDate: time.Now().UTC(), Typ: user.UserV1MFAInitSkippedType, AggregateID: "AggregateID", ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", MFAInitSkipped: time.Now().UTC()}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
{
|
||||
name: "append human mfa init skipped event",
|
||||
args: args{
|
||||
event: &es_models.Event{Seq: 1, CreationDate: time.Now().UTC(), Typ: user.HumanMFAInitSkippedType, AggregateID: "AggregateID", ResourceOwner: "GrantedOrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country"}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "GrantedOrgID", UserName: "UserName", HumanView: &HumanView{FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", MFAInitSkipped: time.Now().UTC()}, State: int32(model.UserStateActive)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.user.AppendEvent(tt.args.event)
|
||||
if tt.args.user.ID != tt.result.ID {
|
||||
t.Errorf("got wrong result ID: expected: %v, actual: %v ", tt.result.ID, tt.args.user.ID)
|
||||
}
|
||||
if tt.args.user.ResourceOwner != tt.result.ResourceOwner {
|
||||
t.Errorf("got wrong result ResourceOwner: expected: %v, actual: %v ", tt.result.ResourceOwner, tt.args.user.ResourceOwner)
|
||||
}
|
||||
if tt.args.user.State != tt.result.State {
|
||||
t.Errorf("got wrong result state: expected: %v, actual: %v ", tt.result.State, tt.args.user.State)
|
||||
}
|
||||
if human := tt.args.user.HumanView; human != nil {
|
||||
if human.FirstName != tt.result.FirstName {
|
||||
t.Errorf("got wrong result FirstName: expected: %v, actual: %v ", tt.result.FirstName, tt.args.user.FirstName)
|
||||
}
|
||||
if human.LastName != tt.result.LastName {
|
||||
t.Errorf("got wrong result FirstName: expected: %v, actual: %v ", tt.result.FirstName, human.FirstName)
|
||||
}
|
||||
if human.Email != tt.result.Email {
|
||||
t.Errorf("got wrong result email: expected: %v, actual: %v ", tt.result.Email, human.Email)
|
||||
}
|
||||
if human.IsEmailVerified != tt.result.IsEmailVerified {
|
||||
t.Errorf("got wrong result IsEmailVerified: expected: %v, actual: %v ", tt.result.IsEmailVerified, human.IsEmailVerified)
|
||||
}
|
||||
if human.Phone != tt.result.Phone {
|
||||
t.Errorf("got wrong result Phone: expected: %v, actual: %v ", tt.result.Phone, human.Phone)
|
||||
}
|
||||
if human.IsPhoneVerified != tt.result.IsPhoneVerified {
|
||||
t.Errorf("got wrong result IsPhoneVerified: expected: %v, actual: %v ", tt.result.IsPhoneVerified, human.IsPhoneVerified)
|
||||
}
|
||||
if human.Country != tt.result.Country {
|
||||
t.Errorf("got wrong result Country: expected: %v, actual: %v ", tt.result.Country, human.Country)
|
||||
}
|
||||
if human.OTPState != tt.result.OTPState {
|
||||
t.Errorf("got wrong result OTPState: expected: %v, actual: %v ", tt.result.OTPState, human.OTPState)
|
||||
}
|
||||
if human.MFAInitSkipped.Round(1*time.Second) != tt.result.MFAInitSkipped.Round(1*time.Second) {
|
||||
t.Errorf("got wrong result MFAInitSkipped: expected: %v, actual: %v ", tt.result.MFAInitSkipped.Round(1*time.Second), human.MFAInitSkipped.Round(1*time.Second))
|
||||
}
|
||||
if human.PasswordSet != tt.result.PasswordSet {
|
||||
t.Errorf("got wrong result PasswordSet: expected: %v, actual: %v ", tt.result.PasswordSet, human.PasswordSet)
|
||||
}
|
||||
if human.PasswordChangeRequired != tt.result.PasswordChangeRequired {
|
||||
t.Errorf("got wrong result PasswordChangeRequired: expected: %v, actual: %v ", tt.result.PasswordChangeRequired, human.PasswordChangeRequired)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
24
apps/api/internal/user/repository/view/query.go
Normal file
24
apps/api/internal/user/repository/view/query.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func UserByIDQuery(id, instanceID string, changeDate time.Time, eventTypes []eventstore.EventType) (*eventstore.SearchQueryBuilder, error) {
|
||||
if id == "" {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "EVENT-d8isw", "Errors.User.UserIDMissing")
|
||||
}
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
InstanceID(instanceID).
|
||||
CreationDateAfter(changeDate.Add(-1 * time.Microsecond)). // to simulate CreationDate >=
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
AggregateIDs(id).
|
||||
EventTypes(eventTypes...).
|
||||
Builder(), nil
|
||||
}
|
50
apps/api/internal/user/repository/view/refresh_token_view.go
Normal file
50
apps/api/internal/user/repository/view/refresh_token_view.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
usr_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func RefreshTokenByID(db *gorm.DB, table, tokenID, instanceID string) (*usr_model.RefreshTokenView, error) {
|
||||
token := new(usr_model.RefreshTokenView)
|
||||
query := repository.PrepareGetByQuery(table,
|
||||
&usr_model.RefreshTokenSearchQuery{Key: model.RefreshTokenSearchKeyRefreshTokenID, Method: domain.SearchMethodEquals, Value: tokenID},
|
||||
&usr_model.RefreshTokenSearchQuery{Key: model.RefreshTokenSearchKeyInstanceID, Method: domain.SearchMethodEquals, Value: instanceID},
|
||||
)
|
||||
err := query(db, token)
|
||||
if zerrors.IsNotFound(err) {
|
||||
return nil, zerrors.ThrowNotFound(nil, "VIEW-6ub3p", "Errors.RefreshToken.NotFound")
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
func RefreshTokensByUserID(db *gorm.DB, table, userID, instanceID string) ([]*usr_model.RefreshTokenView, error) {
|
||||
tokens := make([]*usr_model.RefreshTokenView, 0)
|
||||
userIDQuery := &model.RefreshTokenSearchQuery{
|
||||
Key: model.RefreshTokenSearchKeyUserID,
|
||||
Method: domain.SearchMethodEquals,
|
||||
Value: userID,
|
||||
}
|
||||
instanceIDQuery := &model.RefreshTokenSearchQuery{
|
||||
Key: model.RefreshTokenSearchKeyInstanceID,
|
||||
Method: domain.SearchMethodEquals,
|
||||
Value: instanceID,
|
||||
}
|
||||
query := repository.PrepareSearchQuery(table, usr_model.RefreshTokenSearchRequest{
|
||||
Queries: []*model.RefreshTokenSearchQuery{userIDQuery, instanceIDQuery},
|
||||
})
|
||||
_, err := query(db, &tokens)
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
func SearchRefreshTokens(db *gorm.DB, table string, req *model.RefreshTokenSearchRequest) ([]*usr_model.RefreshTokenView, uint64, error) {
|
||||
tokens := make([]*usr_model.RefreshTokenView, 0)
|
||||
query := repository.PrepareSearchQuery(table, usr_model.RefreshTokenSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
|
||||
count, err := query(db, &tokens)
|
||||
return tokens, count, err
|
||||
}
|
49
apps/api/internal/user/repository/view/token_view.go
Normal file
49
apps/api/internal/user/repository/view/token_view.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
usr_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TokenByIDs(db *gorm.DB, table, tokenID, userID, instanceID string) (*usr_model.TokenView, error) {
|
||||
token := new(usr_model.TokenView)
|
||||
query := repository.PrepareGetByQuery(table,
|
||||
&usr_model.TokenSearchQuery{Key: model.TokenSearchKeyTokenID, Method: domain.SearchMethodEquals, Value: tokenID},
|
||||
&usr_model.TokenSearchQuery{Key: model.TokenSearchKeyUserID, Method: domain.SearchMethodEquals, Value: userID},
|
||||
&usr_model.TokenSearchQuery{Key: model.TokenSearchKeyInstanceID, Method: domain.SearchMethodEquals, Value: instanceID},
|
||||
)
|
||||
err := query(db, token)
|
||||
if zerrors.IsNotFound(err) {
|
||||
return nil, zerrors.ThrowNotFound(nil, "VIEW-6ub3p", "Errors.Token.NotFound")
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
func TokensByUserID(db *gorm.DB, table, userID, instanceID string) ([]*usr_model.TokenView, error) {
|
||||
tokens := make([]*usr_model.TokenView, 0)
|
||||
userIDQuery := &model.TokenSearchQuery{
|
||||
Key: model.TokenSearchKeyUserID,
|
||||
Method: domain.SearchMethodEquals,
|
||||
Value: userID,
|
||||
}
|
||||
instanceIDQuery := &model.TokenSearchQuery{
|
||||
Key: model.TokenSearchKeyInstanceID,
|
||||
Method: domain.SearchMethodEquals,
|
||||
Value: instanceID,
|
||||
}
|
||||
expirationQuery := &model.TokenSearchQuery{
|
||||
Key: model.TokenSearchKeyExpiration,
|
||||
Method: domain.SearchMethodGreaterThan,
|
||||
Value: "now()",
|
||||
}
|
||||
query := repository.PrepareSearchQuery(table, usr_model.TokenSearchRequest{
|
||||
Queries: []*model.TokenSearchQuery{userIDQuery, instanceIDQuery, expirationQuery},
|
||||
})
|
||||
_, err := query(db, &tokens)
|
||||
return tokens, err
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
SELECT
|
||||
s.user_agent_id
|
||||
FROM auth.user_sessions s
|
||||
WHERE
|
||||
s.id = $1
|
||||
AND s.instance_id = $2
|
||||
LIMIT 1;
|
97
apps/api/internal/user/repository/view/user_by_id.sql
Normal file
97
apps/api/internal/user/repository/view/user_by_id.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
WITH auth_methods AS (
|
||||
SELECT
|
||||
user_id
|
||||
, method_type
|
||||
, token_id
|
||||
, state
|
||||
, instance_id
|
||||
, name
|
||||
FROM
|
||||
projections.user_auth_methods5
|
||||
WHERE
|
||||
instance_id = $1
|
||||
AND user_id = $2
|
||||
),
|
||||
verified_auth_methods AS (
|
||||
SELECT
|
||||
method_type
|
||||
FROM
|
||||
auth_methods
|
||||
WHERE state = 2
|
||||
)
|
||||
SELECT
|
||||
u.id
|
||||
, u.creation_date
|
||||
, LEAST(u.change_date, au.change_date) AS change_date
|
||||
, u.resource_owner
|
||||
, u.state AS user_state
|
||||
, au.password_set
|
||||
, h.password_change_required
|
||||
, au.password_change
|
||||
, au.last_login
|
||||
, u.username AS user_name
|
||||
, (SELECT array_agg(ll.login_name) login_names FROM projections.login_names3 ll
|
||||
WHERE u.instance_id = ll.instance_id AND u.id = ll.user_id
|
||||
GROUP BY ll.user_id, ll.instance_id) AS login_names
|
||||
, l.login_name as preferred_login_name
|
||||
, h.first_name
|
||||
, h.last_name
|
||||
, h.nick_name
|
||||
, h.display_name
|
||||
, h.preferred_language
|
||||
, h.gender
|
||||
, h.email
|
||||
, h.is_email_verified
|
||||
, n.verified_email
|
||||
, h.phone
|
||||
, h.is_phone_verified
|
||||
, (SELECT COALESCE((SELECT state FROM auth_methods WHERE method_type = 1), 0)) AS otp_state
|
||||
, CASE
|
||||
WHEN EXISTS (SELECT true FROM verified_auth_methods WHERE method_type = 3) THEN 2
|
||||
WHEN EXISTS (SELECT true FROM verified_auth_methods WHERE method_type = 2) THEN 1
|
||||
ELSE 0
|
||||
END AS mfa_max_set_up
|
||||
, au.mfa_init_skipped
|
||||
, u.sequence
|
||||
, au.init_required
|
||||
, au.username_change_required
|
||||
, m.name AS machine_name
|
||||
, m.description AS machine_description
|
||||
, u.type AS user_type
|
||||
, (SELECT
|
||||
JSONB_AGG(json_build_object('webAuthNTokenId', token_id, 'webAuthNTokenName', name, 'state', state))
|
||||
FROM auth_methods
|
||||
WHERE method_type = 2
|
||||
) AS u2f_tokens
|
||||
, (SELECT
|
||||
JSONB_AGG(json_build_object('webAuthNTokenId', token_id, 'webAuthNTokenName', name, 'state', state))
|
||||
FROM auth_methods
|
||||
WHERE method_type = 3
|
||||
) AS passwordless_tokens
|
||||
, h.avatar_key
|
||||
, au.passwordless_init_required
|
||||
, au.password_init_required
|
||||
, u.instance_id
|
||||
, (SELECT EXISTS (SELECT true FROM verified_auth_methods WHERE method_type = 6)) AS otp_sms_added
|
||||
, (SELECT EXISTS (SELECT true FROM verified_auth_methods WHERE method_type = 7)) AS otp_email_added
|
||||
FROM projections.users14 u
|
||||
LEFT JOIN projections.users14_humans h
|
||||
ON u.instance_id = h.instance_id
|
||||
AND u.id = h.user_id
|
||||
LEFT JOIN projections.users14_notifications n
|
||||
ON u.instance_id = n.instance_id
|
||||
AND u.id = n.user_id
|
||||
LEFT JOIN projections.login_names3 l
|
||||
ON u.instance_id = l.instance_id
|
||||
AND u.id = l.user_id
|
||||
AND l.is_primary = true
|
||||
LEFT JOIN projections.users14_machines m
|
||||
ON u.instance_id = m.instance_id
|
||||
AND u.id = m.user_id
|
||||
LEFT JOIN auth.users3 au
|
||||
ON u.instance_id = au.instance_id
|
||||
AND u.id = au.id
|
||||
WHERE
|
||||
u.instance_id = $1
|
||||
AND u.id = $2
|
||||
LIMIT 1;
|
29
apps/api/internal/user/repository/view/user_session.sql
Normal file
29
apps/api/internal/user/repository/view/user_session.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
SELECT s.creation_date,
|
||||
s.change_date,
|
||||
s.resource_owner,
|
||||
s.state,
|
||||
s.user_agent_id,
|
||||
s.user_id,
|
||||
u.username,
|
||||
l.login_name,
|
||||
h.display_name,
|
||||
h.avatar_key,
|
||||
s.selected_idp_config_id,
|
||||
s.password_verification,
|
||||
s.passwordless_verification,
|
||||
s.external_login_verification,
|
||||
s.second_factor_verification,
|
||||
s.second_factor_verification_type,
|
||||
s.multi_factor_verification,
|
||||
s.multi_factor_verification_type,
|
||||
s.sequence,
|
||||
s.instance_id,
|
||||
s.id
|
||||
FROM auth.user_sessions s
|
||||
LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id
|
||||
LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
|
||||
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
|
||||
WHERE (s.id = $1)
|
||||
AND (s.instance_id = $2)
|
||||
LIMIT 1
|
||||
;
|
@@ -0,0 +1,30 @@
|
||||
SELECT s.creation_date,
|
||||
s.change_date,
|
||||
s.resource_owner,
|
||||
s.state,
|
||||
s.user_agent_id,
|
||||
s.user_id,
|
||||
u.username,
|
||||
l.login_name,
|
||||
h.display_name,
|
||||
h.avatar_key,
|
||||
s.selected_idp_config_id,
|
||||
s.password_verification,
|
||||
s.passwordless_verification,
|
||||
s.external_login_verification,
|
||||
s.second_factor_verification,
|
||||
s.second_factor_verification_type,
|
||||
s.multi_factor_verification,
|
||||
s.multi_factor_verification_type,
|
||||
s.sequence,
|
||||
s.instance_id,
|
||||
s.id
|
||||
FROM auth.user_sessions s
|
||||
LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id
|
||||
LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
|
||||
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
|
||||
WHERE (s.user_agent_id = $1)
|
||||
AND (s.user_id = $2)
|
||||
AND (s.instance_id = $3)
|
||||
LIMIT 1
|
||||
;
|
188
apps/api/internal/user/repository/view/user_session_view.go
Normal file
188
apps/api/internal/user/repository/view/user_session_view.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"errors"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
//go:embed user_session_by_id.sql
|
||||
var userSessionByIDsQuery string
|
||||
|
||||
//go:embed user_session.sql
|
||||
var userSessionByIDQuery string
|
||||
|
||||
//go:embed user_sessions_by_user_agent.sql
|
||||
var userSessionsByUserAgentQuery string
|
||||
|
||||
//go:embed user_agent_by_user_session_id.sql
|
||||
var userAgentByUserSessionIDQuery string
|
||||
|
||||
//go:embed active_user_sessions_by_session_id.sql
|
||||
var activeUserSessionsBySessionIDQuery string
|
||||
|
||||
func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, instanceID string) (userSession *model.UserSessionView, err error) {
|
||||
err = db.QueryRowContext(
|
||||
ctx,
|
||||
func(row *sql.Row) error {
|
||||
userSession, err = scanUserSession(row)
|
||||
return err
|
||||
},
|
||||
userSessionByIDsQuery,
|
||||
agentID,
|
||||
userID,
|
||||
instanceID,
|
||||
)
|
||||
return userSession, err
|
||||
}
|
||||
|
||||
func UserSessionByID(ctx context.Context, db *database.DB, userSessionID, instanceID string) (userSession *model.UserSessionView, err error) {
|
||||
err = db.QueryRowContext(
|
||||
ctx,
|
||||
func(row *sql.Row) error {
|
||||
userSession, err = scanUserSession(row)
|
||||
return err
|
||||
},
|
||||
userSessionByIDQuery,
|
||||
userSessionID,
|
||||
instanceID,
|
||||
)
|
||||
return userSession, err
|
||||
}
|
||||
|
||||
func UserSessionsByAgentID(ctx context.Context, db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) {
|
||||
err = db.QueryContext(
|
||||
ctx,
|
||||
func(rows *sql.Rows) error {
|
||||
userSessions, err = scanUserSessions(rows)
|
||||
return err
|
||||
},
|
||||
userSessionsByUserAgentQuery,
|
||||
agentID,
|
||||
instanceID,
|
||||
)
|
||||
return userSessions, err
|
||||
}
|
||||
|
||||
func UserAgentIDBySessionID(ctx context.Context, db *database.DB, sessionID, instanceID string) (userAgentID string, err error) {
|
||||
err = db.QueryRowContext(
|
||||
ctx,
|
||||
func(row *sql.Row) error {
|
||||
return row.Scan(&userAgentID)
|
||||
},
|
||||
userAgentByUserSessionIDQuery,
|
||||
sessionID,
|
||||
instanceID,
|
||||
)
|
||||
return userAgentID, err
|
||||
}
|
||||
|
||||
// ActiveUserSessionsBySessionID returns all sessions (sessionID:userID map) with an active session on the same user agent (its id is also returned) based on a sessionID
|
||||
func ActiveUserSessionsBySessionID(ctx context.Context, db *database.DB, sessionID, instanceID string) (userAgentID string, sessions map[string]string, err error) {
|
||||
err = db.QueryContext(
|
||||
ctx,
|
||||
func(rows *sql.Rows) error {
|
||||
userAgentID, sessions, err = scanActiveUserAgentUserIDs(rows)
|
||||
return err
|
||||
},
|
||||
activeUserSessionsBySessionIDQuery,
|
||||
sessionID,
|
||||
instanceID,
|
||||
)
|
||||
return userAgentID, sessions, err
|
||||
}
|
||||
|
||||
func scanActiveUserAgentUserIDs(rows *sql.Rows) (userAgentID string, sessions map[string]string, err error) {
|
||||
sessions = make(map[string]string)
|
||||
for rows.Next() {
|
||||
var userID, sessionID string
|
||||
err := rows.Scan(
|
||||
&userAgentID,
|
||||
&userID,
|
||||
&sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
sessions[sessionID] = userID
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return "", nil, zerrors.ThrowInternal(err, "VIEW-Sbrws", "Errors.Query.CloseRows")
|
||||
}
|
||||
return userAgentID, sessions, nil
|
||||
}
|
||||
|
||||
func scanUserSession(row *sql.Row) (*model.UserSessionView, error) {
|
||||
session := new(model.UserSessionView)
|
||||
err := row.Scan(
|
||||
&session.CreationDate,
|
||||
&session.ChangeDate,
|
||||
&session.ResourceOwner,
|
||||
&session.State,
|
||||
&session.UserAgentID,
|
||||
&session.UserID,
|
||||
&session.UserName,
|
||||
&session.LoginName,
|
||||
&session.DisplayName,
|
||||
&session.AvatarKey,
|
||||
&session.SelectedIDPConfigID,
|
||||
&session.PasswordVerification,
|
||||
&session.PasswordlessVerification,
|
||||
&session.ExternalLoginVerification,
|
||||
&session.SecondFactorVerification,
|
||||
&session.SecondFactorVerificationType,
|
||||
&session.MultiFactorVerification,
|
||||
&session.MultiFactorVerificationType,
|
||||
&session.Sequence,
|
||||
&session.InstanceID,
|
||||
&session.ID,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, zerrors.ThrowNotFound(nil, "VIEW-NGBs1", "Errors.UserSession.NotFound")
|
||||
}
|
||||
return session, err
|
||||
}
|
||||
|
||||
func scanUserSessions(rows *sql.Rows) ([]*model.UserSessionView, error) {
|
||||
sessions := make([]*model.UserSessionView, 0)
|
||||
for rows.Next() {
|
||||
session := new(model.UserSessionView)
|
||||
err := rows.Scan(
|
||||
&session.CreationDate,
|
||||
&session.ChangeDate,
|
||||
&session.ResourceOwner,
|
||||
&session.State,
|
||||
&session.UserAgentID,
|
||||
&session.UserID,
|
||||
&session.UserName,
|
||||
&session.LoginName,
|
||||
&session.DisplayName,
|
||||
&session.AvatarKey,
|
||||
&session.SelectedIDPConfigID,
|
||||
&session.PasswordVerification,
|
||||
&session.PasswordlessVerification,
|
||||
&session.ExternalLoginVerification,
|
||||
&session.SecondFactorVerification,
|
||||
&session.SecondFactorVerificationType,
|
||||
&session.MultiFactorVerification,
|
||||
&session.MultiFactorVerificationType,
|
||||
&session.Sequence,
|
||||
&session.InstanceID,
|
||||
&session.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "VIEW-FSF3g", "Errors.Query.CloseRows")
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
SELECT s.creation_date,
|
||||
s.change_date,
|
||||
s.resource_owner,
|
||||
s.state,
|
||||
s.user_agent_id,
|
||||
s.user_id,
|
||||
u.username,
|
||||
l.login_name,
|
||||
h.display_name,
|
||||
h.avatar_key,
|
||||
s.selected_idp_config_id,
|
||||
s.password_verification,
|
||||
s.passwordless_verification,
|
||||
s.external_login_verification,
|
||||
s.second_factor_verification,
|
||||
s.second_factor_verification_type,
|
||||
s.multi_factor_verification,
|
||||
s.multi_factor_verification_type,
|
||||
s.sequence,
|
||||
s.instance_id,
|
||||
s.id
|
||||
FROM auth.user_sessions s
|
||||
LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id
|
||||
LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id
|
||||
LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true
|
||||
WHERE (s.user_agent_id = $1 and s.user_agent_id <> '')
|
||||
AND (s.instance_id = $2)
|
||||
;
|
42
apps/api/internal/user/repository/view/user_view.go
Normal file
42
apps/api/internal/user/repository/view/user_view.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"errors"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
//go:embed user_by_id.sql
|
||||
var userByIDQuery string
|
||||
|
||||
func UserByID(ctx context.Context, db *gorm.DB, userID, instanceID string) (*model.UserView, error) {
|
||||
user := new(model.UserView)
|
||||
|
||||
query := db.Raw(userByIDQuery, instanceID, userID)
|
||||
|
||||
tx := query.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
defer func() {
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
logging.OnError(err).Info("commit failed")
|
||||
}
|
||||
tx.RollbackUnlessCommitted()
|
||||
}()
|
||||
|
||||
err := tx.Scan(user).Error
|
||||
if err == nil {
|
||||
user.SetEmptyUserType()
|
||||
return user, nil
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, zerrors.ThrowNotFound(err, "VIEW-hodc6", "Errors.User.NotFound")
|
||||
}
|
||||
logging.WithError(err).Warn("unable to get user by id")
|
||||
return nil, zerrors.ThrowInternal(err, "VIEW-qJBg9", "unable to get user by id")
|
||||
}
|
Reference in New Issue
Block a user