mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-01 15:53:42 +00:00
move SetEmail buisiness logic into command
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ type Email struct {
|
||||
|
||||
EmailAddress EmailAddress
|
||||
IsEmailVerified bool
|
||||
PlainCode *string
|
||||
}
|
||||
|
||||
type EmailCode struct {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -122,9 +122,10 @@ func HumanEmailVerificationFailedEventMapper(event *repository.Event) (eventstor
|
||||
type HumanEmailCodeAddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
URLTemplate *string `json:"url_template,omitempty"`
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user