move SetEmail buisiness logic into command

This commit is contained in:
Tim Möhlmann
2023-04-20 19:58:48 +03:00
parent 2371db1a18
commit 37b99e9be4
10 changed files with 142 additions and 77 deletions

View File

@@ -2,27 +2,36 @@ package user
import (
"context"
"io"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
grpcContext "github.com/zitadel/zitadel/pkg/grpc/context/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) prepareSetEmail(ctx context.Context, req *user.SetEmailRequest) (cmd *command.UserEmail, err error) {
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
var email *domain.Email
switch v := req.GetVerification().(type) {
case nil, *user.SetEmailRequest_ReturnCode, *user.SetEmailRequest_IsVerified:
break
case *user.SetEmailRequest_SendCode:
// test execute the template to ensure it's valid
err = domain.RenderConfirmURLTemplate(io.Discard, v.SendCode.GetUrlTemplate(), req.UserId, "code", "orgID")
email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
case *user.SetEmailRequest_ReturnCode:
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
case *user.SetEmailRequest_IsVerified:
if v.IsVerified {
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), resourceOwner, req.GetEmail())
} else {
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
}
case nil:
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
default:
err = caos_errs.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v)
}
@@ -30,14 +39,6 @@ func (s *Server) prepareSetEmail(ctx context.Context, req *user.SetEmailRequest)
return nil, err
}
return s.command.UserEmail(ctx, req.UserId, authz.GetCtxData(ctx).ResourceOwner)
}
func finalizeSetEmail(ctx context.Context, cmd *command.UserEmail, verificationCode *string) (resp *user.SetEmailResponse, err error) {
email, err := cmd.Push(ctx)
if err != nil {
return nil, err
}
return &user.SetEmailResponse{
Details: &grpcContext.ObjectDetails{
Sequence: email.Sequence,
@@ -45,39 +46,10 @@ func finalizeSetEmail(ctx context.Context, cmd *command.UserEmail, verificationC
ChangeDate: timestamppb.New(email.ChangeDate),
ResourceOwner: email.ResourceOwner,
},
VerificationCode: verificationCode,
VerificationCode: email.PlainCode,
}, nil
}
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
cmd, err := s.prepareSetEmail(ctx, req)
if err != nil {
return nil, err
}
if err = cmd.Change(ctx, domain.EmailAddress(req.Email)); err != nil {
return nil, err
}
if req.GetIsVerified() {
cmd.SetVerified(ctx)
return finalizeSetEmail(ctx, cmd, nil)
}
generator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, s.userCodeAlg)
if err != nil {
return nil, err
}
code, plainTextCode, err := domain.NewEmailCode(generator)
if err != nil {
return nil, err
}
cmd.AddCode(ctx, code, req.GetSendCode().UrlTemplate)
if req.GetReturnCode() != nil {
return finalizeSetEmail(ctx, cmd, &plainTextCode)
}
return finalizeSetEmail(ctx, cmd, nil)
}
func (s *Server) VerifyEmail(context.Context, *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method VerifyEmail not implemented")
}

View File

@@ -10,24 +10,33 @@ import (
"github.com/zitadel/zitadel/internal/errors"
)
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, expiry time.Duration, err error) {
type cryptoCode struct {
value *crypto.CryptoValue
plain string
expiry time.Duration
}
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*cryptoCode, error) {
config, err := secretGeneratorConfig(ctx, filter, typ)
if err != nil {
return nil, -1, err
return nil, err
}
code := &cryptoCode{
expiry: config.Expiry,
}
switch a := alg.(type) {
case crypto.HashAlgorithm:
value, _, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
code.value, code.plain, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
case crypto.EncryptionAlgorithm:
value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
code.value, code.plain, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
default:
return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
return nil, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
}
if err != nil {
return nil, -1, err
return nil, err
}
return value, config.Expiry, nil
return code, nil
}
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {

View File

@@ -2,7 +2,6 @@ package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
@@ -18,6 +17,6 @@ func (e *Email) Validate() error {
return e.Address.Validate()
}
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*cryptoCode, error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
}

View File

@@ -2,7 +2,6 @@ package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
@@ -14,6 +13,6 @@ type Phone struct {
Verified bool
}
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*cryptoCode, error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
}

View File

@@ -439,7 +439,7 @@ func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id
return exists, nil
}
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*cryptoCode, error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
}

View File

@@ -202,29 +202,29 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
// email not verified or
// user not registered and password set
if human.shouldAddInitCode() {
value, expiry, err := newUserInitCode(ctx, filter, codeAlg)
userCode, err := newUserInitCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, userCode.value, userCode.expiry))
} else {
if !human.Email.Verified {
value, expiry, err := newEmailCode(ctx, filter, codeAlg)
emailCode, err := newEmailCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, emailCode.value, emailCode.expiry))
}
}
if human.Phone.Verified {
cmds = append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate))
} else if human.Phone.Number != "" {
value, expiry, err := newPhoneCode(ctx, filter, codeAlg)
phoneCode, err := newPhoneCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, phoneCode.value, phoneCode.expiry))
}
return cmds, nil

View File

@@ -2,21 +2,82 @@ package command
import (
"context"
"io"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
)
type UserEmail struct {
// ChangeUserEmail sets a user's email address, generates a code
// and triggers a notification e-mail with the default confirmation URL format.
func (c *Commands) ChangeUserEmail(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) {
return c.changeUserEmail(ctx, userID, resourceOwner, email, alg, false, nil)
}
// ChangeUserEmailURLTemplate sets a user's email address, generates a code
// and triggers a notification e-mail with the confirmation URL rendered from the passed urlTmpl.
// urlTmpl must be a valid [tmpl.Template].
func (c *Commands) ChangeUserEmailURLTemplate(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, urlTmpl string) (*domain.Email, error) {
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTmpl, userID, "code", "orgID"); err != nil {
return nil, err
}
return c.changeUserEmail(ctx, userID, resourceOwner, email, alg, false, &urlTmpl)
}
// ChangeUserEmailReturnCode sets a user's email address, generates a code and does not send a notification email.
// The generated plain text code will be set in the returned Email object.
func (c *Commands) ChangeUserEmailReturnCode(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) {
return c.changeUserEmail(ctx, userID, resourceOwner, email, alg, true, nil)
}
// ChangeUserEmailVerified sets a user's email address and marks it is verified.
// No code is generated and no confirmation e-mail is send.
func (c *Commands) ChangeUserEmailVerified(ctx context.Context, userID, resourceOwner, email string) (*domain.Email, error) {
return c.changeUserEmail(ctx, userID, resourceOwner, email, nil, false, nil)
}
// changeUserEmail set a user's email address.
// When alg is nil, the email address is set as verified and the remainder of options are ignored.
// returnCode controls if the plain text version of the code will be set in the return object.
// When the plain text code is returned, no notification e-mail will be send to the user.
// urlTmpl allows changing the target URL that is used by the e-mail annd should be a valid Go template.
func (c *Commands) changeUserEmail(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, returnCode bool, urlTmpl *string) (*domain.Email, error) {
cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil {
return nil, err
}
if alg == nil {
cmd.SetVerified(ctx)
return cmd.Push(ctx)
}
if err = cmd.AddGeneratedCode(ctx, alg, urlTmpl, returnCode); err != nil {
return nil, err
}
return cmd.Push(ctx)
}
// UserEmailEvents allows step-by-step additions of events,
// operating on the Human Email Model.
type UserEmailEvents struct {
eventstore *eventstore.Eventstore
aggregate *eventstore.Aggregate
events []eventstore.Command
model *HumanEmailWriteModel
plainCode *string
}
func (c *Commands) UserEmail(ctx context.Context, userID, resourceOwner string) (*UserEmail, error) {
// NewUserEmailEvents constructs a UserEmailEvents with a Human Email Write Model,
// filtered by userID and resourceOwner.
// If a model cannot be found, or it's state is invalid and error is returned.
func (c *Commands) NewUserEmailEvents(ctx context.Context, userID, resourceOwner string) (*UserEmailEvents, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing")
}
@@ -31,14 +92,16 @@ func (c *Commands) UserEmail(ctx context.Context, userID, resourceOwner string)
if model.UserState == domain.UserStateInitial {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-J8dsk", "Errors.User.NotInitialised")
}
return &UserEmail{
return &UserEmailEvents{
eventstore: c.eventstore,
aggregate: UserAggregateFromWriteModel(&model.WriteModel),
model: model,
}, nil
}
func (c *UserEmail) Change(ctx context.Context, email domain.EmailAddress) error {
// Change sets a new email address.
// The generated event unsets any previously generated code and verified flag.
func (c *UserEmailEvents) Change(ctx context.Context, email domain.EmailAddress) error {
if err := email.Validate(); err != nil {
return err
}
@@ -50,15 +113,27 @@ func (c *UserEmail) Change(ctx context.Context, email domain.EmailAddress) error
return nil
}
func (c *UserEmail) SetVerified(ctx context.Context) {
// SetVerified sets the email address to verified.
func (c *UserEmailEvents) SetVerified(ctx context.Context) {
c.events = append(c.events, user.NewHumanEmailVerifiedEvent(ctx, c.aggregate))
}
func (c *UserEmail) AddCode(ctx context.Context, code *domain.EmailCode, urlTmpl *string) {
c.events = append(c.events, user.NewHumanEmailCodeAddedEventV2(ctx, c.aggregate, code.Code, code.Expiry, urlTmpl))
// AddGeneratedCode generates a new encrypted code and sets it to the email address.
// When returnCode a plain text of the code will be returned from Push.
func (c *UserEmailEvents) AddGeneratedCode(ctx context.Context, alg crypto.EncryptionAlgorithm, urlTmpl *string, returnCode bool) error {
code, err := newEmailCode(ctx, c.eventstore.Filter, alg)
if err != nil {
return err
}
c.events = append(c.events, user.NewHumanEmailCodeAddedEventV2(ctx, c.aggregate, code.value, code.expiry, urlTmpl, returnCode))
if returnCode {
c.plainCode = &code.plain
}
return nil
}
func (c *UserEmail) Push(ctx context.Context) (*domain.Email, error) {
// Push all events to the eventstore and Reduce them into the Model.
func (c *UserEmailEvents) Push(ctx context.Context) (*domain.Email, error) {
pushedEvents, err := c.eventstore.Push(ctx, c.events...)
if err != nil {
return nil, err
@@ -67,5 +142,8 @@ func (c *UserEmail) Push(ctx context.Context) (*domain.Email, error) {
if err != nil {
return nil, err
}
return writeModelToEmail(c.model), nil
email := writeModelToEmail(c.model)
email.PlainCode = c.plainCode
return email, nil
}

View File

@@ -38,6 +38,7 @@ type Email struct {
EmailAddress EmailAddress
IsEmailVerified bool
PlainCode *string
}
type EmailCode struct {

View File

@@ -182,6 +182,10 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
}
if e.CodeReturned {
return crdb.NewNoOpStatement(e), nil
}
ctx := HandlerContext(event.Aggregate())
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,

View File

@@ -125,6 +125,7 @@ type HumanEmailCodeAddedEvent struct {
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
URLTemplate *string `json:"url_template,omitempty"`
CodeReturned bool `json:"code_returned,omitempty"`
}
func (e *HumanEmailCodeAddedEvent) Data() interface{} {
@@ -141,7 +142,7 @@ func NewHumanEmailCodeAddedEvent(
code *crypto.CryptoValue,
expiry time.Duration,
) *HumanEmailCodeAddedEvent {
return NewHumanEmailCodeAddedEventV2(ctx, aggregate, code, expiry, nil)
return NewHumanEmailCodeAddedEventV2(ctx, aggregate, code, expiry, nil, false)
}
func NewHumanEmailCodeAddedEventV2(
@@ -149,7 +150,9 @@ func NewHumanEmailCodeAddedEventV2(
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
urlTemplate *string) *HumanEmailCodeAddedEvent {
urlTemplate *string,
codeReturned bool,
) *HumanEmailCodeAddedEvent {
return &HumanEmailCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,