mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:47:32 +00:00
feat: invite user link (#8578)
# 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
This commit is contained in:
141
internal/command/user_v2_invite_model.go
Normal file
141
internal/command/user_v2_invite_model.go
Normal file
@@ -0,0 +1,141 @@
|
||||
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)
|
||||
}
|
Reference in New Issue
Block a user