mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-14 20:08:02 +00:00
a07b2f4677
# Which Problems Are Solved As an administrator I want to be able to invite users to my application with the API V2, some user data I will already prefil, the user should add the authentication method themself (password, passkey, sso). # How the Problems Are Solved - A user can now be created with a email explicitly set to false. - If a user has no verified email and no authentication method, an `InviteCode` can be created through the User V2 API. - the code can be returned or sent through email - additionally `URLTemplate` and an `ApplicatioName` can provided for the email - The code can be resent and verified through the User V2 API - The V1 login allows users to verify and resend the code and set a password (analog user initialization) - The message text for the user invitation can be customized # Additional Changes - `verifyUserPasskeyCode` directly uses `crypto.VerifyCode` (instead of `verifyEncryptedCode`) - `verifyEncryptedCode` is removed (unnecessarily queried for the code generator) # Additional Context - closes #8310 - TODO: login V2 will have to implement invite flow: https://github.com/zitadel/typescript/issues/166
142 lines
4.1 KiB
Go
142 lines
4.1 KiB
Go
package command
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
)
|
|
|
|
type UserV2InviteWriteModel struct {
|
|
eventstore.WriteModel
|
|
|
|
InviteCode *crypto.CryptoValue
|
|
InviteCodeCreationDate time.Time
|
|
InviteCodeExpiry time.Duration
|
|
InviteCheckFailureCount uint8
|
|
|
|
ApplicationName string
|
|
AuthRequestID string
|
|
URLTemplate string
|
|
CodeReturned bool
|
|
EmailVerified bool
|
|
AuthMethodSet bool
|
|
|
|
UserState domain.UserState
|
|
}
|
|
|
|
func (wm *UserV2InviteWriteModel) CreationAllowed() bool {
|
|
return !wm.EmailVerified && !wm.AuthMethodSet
|
|
}
|
|
|
|
func newUserV2InviteWriteModel(userID, orgID string) *UserV2InviteWriteModel {
|
|
return &UserV2InviteWriteModel{
|
|
WriteModel: eventstore.WriteModel{
|
|
AggregateID: userID,
|
|
ResourceOwner: orgID,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (wm *UserV2InviteWriteModel) Reduce() error {
|
|
for _, event := range wm.Events {
|
|
switch e := event.(type) {
|
|
case *user.HumanAddedEvent:
|
|
wm.UserState = domain.UserStateActive
|
|
wm.AuthMethodSet = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) != ""
|
|
wm.EmptyInviteCode()
|
|
wm.ApplicationName = ""
|
|
wm.AuthRequestID = ""
|
|
case *user.HumanRegisteredEvent:
|
|
wm.UserState = domain.UserStateActive
|
|
wm.AuthMethodSet = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) != ""
|
|
wm.EmptyInviteCode()
|
|
wm.ApplicationName = ""
|
|
wm.AuthRequestID = ""
|
|
case *user.HumanInviteCodeAddedEvent:
|
|
wm.SetInviteCode(e.Code, e.Expiry, e.CreationDate())
|
|
wm.URLTemplate = e.URLTemplate
|
|
wm.CodeReturned = e.CodeReturned
|
|
wm.ApplicationName = e.ApplicationName
|
|
wm.AuthRequestID = e.AuthRequestID
|
|
case *user.HumanInviteCheckSucceededEvent:
|
|
wm.EmptyInviteCode()
|
|
case *user.HumanInviteCheckFailedEvent:
|
|
wm.InviteCheckFailureCount++
|
|
if wm.InviteCheckFailureCount >= 3 { //TODO: config?
|
|
wm.UserState = domain.UserStateDeleted
|
|
}
|
|
case *user.HumanEmailVerifiedEvent:
|
|
wm.EmailVerified = true
|
|
wm.EmptyInviteCode()
|
|
case *user.UserLockedEvent:
|
|
wm.UserState = domain.UserStateLocked
|
|
case *user.UserUnlockedEvent:
|
|
wm.UserState = domain.UserStateActive
|
|
case *user.UserDeactivatedEvent:
|
|
wm.UserState = domain.UserStateInactive
|
|
case *user.UserReactivatedEvent:
|
|
wm.UserState = domain.UserStateActive
|
|
case *user.UserRemovedEvent:
|
|
wm.UserState = domain.UserStateDeleted
|
|
case *user.HumanPasswordChangedEvent:
|
|
wm.AuthMethodSet = true
|
|
case *user.UserIDPLinkAddedEvent:
|
|
wm.AuthMethodSet = true
|
|
case *user.HumanPasswordlessVerifiedEvent:
|
|
wm.AuthMethodSet = true
|
|
}
|
|
}
|
|
return wm.WriteModel.Reduce()
|
|
}
|
|
|
|
func (wm *UserV2InviteWriteModel) SetInviteCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
|
|
wm.InviteCode = code
|
|
wm.InviteCodeExpiry = expiry
|
|
wm.InviteCodeCreationDate = creationDate
|
|
wm.InviteCheckFailureCount = 0
|
|
}
|
|
|
|
func (wm *UserV2InviteWriteModel) EmptyInviteCode() {
|
|
wm.InviteCode = nil
|
|
wm.InviteCodeExpiry = 0
|
|
wm.InviteCodeCreationDate = time.Time{}
|
|
wm.InviteCheckFailureCount = 0
|
|
}
|
|
func (wm *UserV2InviteWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
|
AddQuery().
|
|
AggregateTypes(user.AggregateType).
|
|
AggregateIDs(wm.AggregateID).
|
|
EventTypes(
|
|
user.UserV1AddedType,
|
|
user.HumanAddedType,
|
|
user.UserV1RegisteredType,
|
|
user.HumanRegisteredType,
|
|
user.HumanInviteCodeAddedType,
|
|
user.HumanInviteCheckSucceededType,
|
|
user.HumanInviteCheckFailedType,
|
|
user.UserV1EmailVerifiedType,
|
|
user.HumanEmailVerifiedType,
|
|
user.UserLockedType,
|
|
user.UserUnlockedType,
|
|
user.UserDeactivatedType,
|
|
user.UserReactivatedType,
|
|
user.UserRemovedType,
|
|
user.HumanPasswordChangedType,
|
|
user.UserV1PasswordChangedType,
|
|
user.UserIDPLinkAddedType,
|
|
user.HumanPasswordlessTokenVerifiedType,
|
|
).Builder()
|
|
if wm.ResourceOwner != "" {
|
|
query.ResourceOwner(wm.ResourceOwner)
|
|
}
|
|
return query
|
|
}
|
|
|
|
func (wm *UserV2InviteWriteModel) Aggregate() *user.Aggregate {
|
|
return user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
|
}
|