mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 10:57:35 +00:00
chore: move the go code into a subfolder
This commit is contained in:
@@ -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