From f6995fcb6cc8998d17b9bcee41469cb4dd684444 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 14 Feb 2024 08:22:55 +0100 Subject: [PATCH] feat: add implementation for resend of email and phone code (#7348) * fix: add implementation for resend of email and phone code * fix: add implementation for resend of email and phone code * fix: add implementation for resend of email and phone code * fix: add implementation for resend of email and phone code * fix: add implementation for resend of email and phone code * fix: add implementation for resend of email and phone code * fix: apply suggestions from code review Co-authored-by: Livio Spring * fix: review changes to remove resourceowner as parameters --------- Co-authored-by: Livio Spring --- internal/api/grpc/user/v2/email.go | 37 +- .../grpc/user/v2/email_integration_test.go | 111 +++ internal/api/grpc/user/v2/phone.go | 36 +- .../grpc/user/v2/phone_integration_test.go | 76 ++ internal/api/ui/login/mfa_init_sms.go | 4 +- internal/command/user_v2_email.go | 98 +- internal/command/user_v2_email_test.go | 911 +++++++++++++++--- internal/command/user_v2_phone.go | 75 +- internal/command/user_v2_phone_test.go | 585 +++++++++-- internal/static/i18n/bg.yaml | 1 + internal/static/i18n/cs.yaml | 1 + internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/es.yaml | 1 + internal/static/i18n/fr.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/i18n/ja.yaml | 1 + internal/static/i18n/nl.yaml | 1 + internal/static/i18n/pl.yaml | 1 + internal/static/i18n/pt.yaml | 1 + internal/static/i18n/ru.yaml | 1 + internal/static/i18n/zh.yaml | 1 + proto/zitadel/user/v2beta/user_service.proto | 96 ++ 23 files changed, 1788 insertions(+), 254 deletions(-) diff --git a/internal/api/grpc/user/v2/email.go b/internal/api/grpc/user/v2/email.go index 7936fa4d1d..38cc73c75c 100644 --- a/internal/api/grpc/user/v2/email.go +++ b/internal/api/grpc/user/v2/email.go @@ -12,18 +12,17 @@ import ( ) func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) { - var resourceOwner string // TODO: check if still needed var email *domain.Email switch v := req.GetVerification().(type) { case *user.SetEmailRequest_SendCode: - email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) case *user.SetEmailRequest_ReturnCode: - email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg) + email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg) case *user.SetEmailRequest_IsVerified: - email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), resourceOwner, req.GetEmail()) + email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), req.GetEmail()) case nil: - email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg) + email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v) } @@ -41,10 +40,36 @@ func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp }, nil } +func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.ResendEmailCodeResponse, err error) { + var email *domain.Email + + switch v := req.GetVerification().(type) { + case *user.ResendEmailCodeRequest_SendCode: + email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + case *user.ResendEmailCodeRequest_ReturnCode: + email, err = s.command.ResendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + case nil: + email, err = s.command.ResendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg) + default: + err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method ResendEmailCode not implemented", v) + } + if err != nil { + return nil, err + } + + return &user.ResendEmailCodeResponse{ + Details: &object.Details{ + Sequence: email.Sequence, + ChangeDate: timestamppb.New(email.ChangeDate), + ResourceOwner: email.ResourceOwner, + }, + VerificationCode: email.PlainCode, + }, nil +} + func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) { details, err := s.command.VerifyUserEmail(ctx, req.GetUserId(), - "", // TODO: check if still needed req.GetVerificationCode(), s.userCodeAlg, ) diff --git a/internal/api/grpc/user/v2/email_integration_test.go b/internal/api/grpc/user/v2/email_integration_test.go index 53abd7c850..4034a5e7da 100644 --- a/internal/api/grpc/user/v2/email_integration_test.go +++ b/internal/api/grpc/user/v2/email_integration_test.go @@ -3,7 +3,9 @@ package user_test import ( + "fmt" "testing" + "time" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" @@ -24,6 +26,14 @@ func TestServer_SetEmail(t *testing.T) { want *user.SetEmailResponse wantErr bool }{ + { + name: "user not existing", + req: &user.SetEmailRequest{ + UserId: "xxx", + Email: "default-verifier@mouse.com", + }, + wantErr: true, + }, { name: "default verfication", req: &user.SetEmailRequest{ @@ -133,6 +143,107 @@ func TestServer_SetEmail(t *testing.T) { } } +func TestServer_ResendEmailCode(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + verifiedUserID := Tester.CreateHumanUserVerified(CTX, Tester.Organisation.ID, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId() + + tests := []struct { + name string + req *user.ResendEmailCodeRequest + want *user.ResendEmailCodeResponse + wantErr bool + }{ + { + name: "user not existing", + req: &user.ResendEmailCodeRequest{ + UserId: "xxx", + }, + wantErr: true, + }, + { + name: "user no code", + req: &user.ResendEmailCodeRequest{ + UserId: verifiedUserID, + }, + wantErr: true, + }, + { + name: "resend", + req: &user.ResendEmailCodeRequest{ + UserId: userID, + }, + want: &user.ResendEmailCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "custom url template", + req: &user.ResendEmailCodeRequest{ + UserId: userID, + Verification: &user.ResendEmailCodeRequest_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + want: &user.ResendEmailCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "template error", + req: &user.ResendEmailCodeRequest{ + UserId: userID, + Verification: &user.ResendEmailCodeRequest_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + wantErr: true, + }, + { + name: "return code", + req: &user.ResendEmailCodeRequest{ + UserId: userID, + Verification: &user.ResendEmailCodeRequest_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + want: &user.ResendEmailCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + VerificationCode: gu.Ptr("xxx"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ResendEmailCode(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + if tt.want.GetVerificationCode() != "" { + assert.NotEmpty(t, got.GetVerificationCode()) + } + }) + } +} + func TestServer_VerifyEmail(t *testing.T) { userResp := Tester.CreateHumanUser(CTX) tests := []struct { diff --git a/internal/api/grpc/user/v2/phone.go b/internal/api/grpc/user/v2/phone.go index b2df7d2c6c..5024768b35 100644 --- a/internal/api/grpc/user/v2/phone.go +++ b/internal/api/grpc/user/v2/phone.go @@ -12,18 +12,17 @@ import ( ) func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) { - var resourceOwner string // TODO: check if still needed var phone *domain.Phone switch v := req.GetVerification().(type) { case *user.SetPhoneRequest_SendCode: - phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), resourceOwner, req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) case *user.SetPhoneRequest_ReturnCode: - phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) case *user.SetPhoneRequest_IsVerified: - phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), resourceOwner, req.GetPhone()) + phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), req.GetPhone()) case nil: - phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), resourceOwner, req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v) } @@ -41,10 +40,35 @@ func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp }, nil } +func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) { + var phone *domain.Phone + switch v := req.GetVerification().(type) { + case *user.ResendPhoneCodeRequest_SendCode: + phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg) + case *user.ResendPhoneCodeRequest_ReturnCode: + phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + case nil: + phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg) + default: + err = zerrors.ThrowUnimplementedf(nil, "USERv2-ResendUserPhoneCode", "verification oneOf %T in method SetPhone not implemented", v) + } + if err != nil { + return nil, err + } + + return &user.ResendPhoneCodeResponse{ + Details: &object.Details{ + Sequence: phone.Sequence, + ChangeDate: timestamppb.New(phone.ChangeDate), + ResourceOwner: phone.ResourceOwner, + }, + VerificationCode: phone.PlainCode, + }, nil +} + func (s *Server) VerifyPhone(ctx context.Context, req *user.VerifyPhoneRequest) (*user.VerifyPhoneResponse, error) { details, err := s.command.VerifyUserPhone(ctx, req.GetUserId(), - "", // TODO: check if still needed req.GetVerificationCode(), s.userCodeAlg, ) diff --git a/internal/api/grpc/user/v2/phone_integration_test.go b/internal/api/grpc/user/v2/phone_integration_test.go index 239f86ca9f..ebd885a47e 100644 --- a/internal/api/grpc/user/v2/phone_integration_test.go +++ b/internal/api/grpc/user/v2/phone_integration_test.go @@ -3,7 +3,9 @@ package user_test import ( + "fmt" "testing" + "time" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" @@ -118,6 +120,80 @@ func TestServer_SetPhone(t *testing.T) { } } +func TestServer_ResendPhoneCode(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + verifiedUserID := Tester.CreateHumanUserVerified(CTX, Tester.Organisation.ID, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId() + + tests := []struct { + name string + req *user.ResendPhoneCodeRequest + want *user.ResendPhoneCodeResponse + wantErr bool + }{ + { + name: "user not existing", + req: &user.ResendPhoneCodeRequest{ + UserId: "xxx", + }, + wantErr: true, + }, + { + name: "user not existing", + req: &user.ResendPhoneCodeRequest{ + UserId: verifiedUserID, + }, + wantErr: true, + }, + { + name: "resend code", + req: &user.ResendPhoneCodeRequest{ + UserId: userID, + Verification: &user.ResendPhoneCodeRequest_SendCode{ + SendCode: &user.SendPhoneVerificationCode{}, + }, + }, + want: &user.ResendPhoneCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "return code", + req: &user.ResendPhoneCodeRequest{ + UserId: userID, + Verification: &user.ResendPhoneCodeRequest_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + want: &user.ResendPhoneCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + VerificationCode: gu.Ptr("xxx"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ResendPhoneCode(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + if tt.want.GetVerificationCode() != "" { + assert.NotEmpty(t, got.GetVerificationCode()) + } + }) + } +} + func TestServer_VerifyPhone(t *testing.T) { userResp := Tester.CreateHumanUser(CTX) tests := []struct { diff --git a/internal/api/ui/login/mfa_init_sms.go b/internal/api/ui/login/mfa_init_sms.go index a947918634..9aaa744a39 100644 --- a/internal/api/ui/login/mfa_init_sms.go +++ b/internal/api/ui/login/mfa_init_sms.go @@ -99,7 +99,7 @@ func (l *Login) handleRegisterSMSCheck(w http.ResponseWriter, r *http.Request) { if formData.Code == "" { data.Phone = formData.NewPhone if formData.NewPhone != formData.Phone { - _, err = l.command.ChangeUserPhone(ctx, authReq.UserID, authReq.UserOrgID, formData.NewPhone, l.userCodeAlg) + _, err = l.command.ChangeUserPhone(ctx, authReq.UserID, formData.NewPhone, l.userCodeAlg) if err != nil { // stay in edit more data.Edit = true @@ -109,7 +109,7 @@ func (l *Login) handleRegisterSMSCheck(w http.ResponseWriter, r *http.Request) { return } - _, err = l.command.VerifyUserPhone(ctx, authReq.UserID, authReq.UserOrgID, formData.Code, l.userCodeAlg) + _, err = l.command.VerifyUserPhone(ctx, authReq.UserID, formData.Code, l.userCodeAlg) if err != nil { l.renderRegisterSMS(w, r, authReq, data, err) return diff --git a/internal/command/user_v2_email.go b/internal/command/user_v2_email.go index 3f1b4439e9..6dec15e2b2 100644 --- a/internal/command/user_v2_email.go +++ b/internal/command/user_v2_email.go @@ -16,30 +16,52 @@ import ( // 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.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, false, "") +func (c *Commands) ChangeUserEmail(ctx context.Context, userID, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) { + return c.changeUserEmailWithCode(ctx, userID, email, alg, false, "") } // 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) { +func (c *Commands) ChangeUserEmailURLTemplate(ctx context.Context, userID, 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.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, false, urlTmpl) + return c.changeUserEmailWithCode(ctx, userID, 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.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, true, "") +func (c *Commands) ChangeUserEmailReturnCode(ctx context.Context, userID, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) { + return c.changeUserEmailWithCode(ctx, userID, email, alg, true, "") +} + +// ResendUserEmailCode generates a new code if there is a code existing +// and triggers a notification e-mail with the default confirmation URL format. +func (c *Commands) ResendUserEmailCode(ctx context.Context, userID string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) { + return c.resendUserEmailCode(ctx, userID, alg, false, "") +} + +// ResendUserEmailCodeURLTemplate generates a new code if there is a code existing +// 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) ResendUserEmailCodeURLTemplate(ctx context.Context, userID 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.resendUserEmailCode(ctx, userID, alg, false, urlTmpl) +} + +// ResendUserEmailReturnCode generates a new code if there is a code existing and does not send a notification email. +// The generated plain text code will be set in the returned Email object. +func (c *Commands) ResendUserEmailReturnCode(ctx context.Context, userID string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) { + return c.resendUserEmailCode(ctx, userID, alg, true, "") } // 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) { - cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner) +func (c *Commands) ChangeUserEmailVerified(ctx context.Context, userID, email string) (*domain.Email, error) { + cmd, err := c.NewUserEmailEvents(ctx, userID) if err != nil { return nil, err } @@ -53,29 +75,46 @@ func (c *Commands) ChangeUserEmailVerified(ctx context.Context, userID, resource return cmd.Push(ctx) } -func (c *Commands) changeUserEmailWithCode(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, returnCode bool, urlTmpl string) (*domain.Email, error) { +func (c *Commands) changeUserEmailWithCode(ctx context.Context, userID, email string, alg crypto.EncryptionAlgorithm, returnCode bool, urlTmpl string) (*domain.Email, error) { config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode) if err != nil { return nil, err } gen := crypto.NewEncryptionGenerator(*config, alg) - return c.changeUserEmailWithGenerator(ctx, userID, resourceOwner, email, gen, returnCode, urlTmpl) + return c.changeUserEmailWithGenerator(ctx, userID, email, gen, returnCode, urlTmpl) +} + +func (c *Commands) resendUserEmailCode(ctx context.Context, userID string, alg crypto.EncryptionAlgorithm, returnCode bool, urlTmpl string) (*domain.Email, error) { + config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode) //nolint:staticcheck + if err != nil { + return nil, err + } + gen := crypto.NewEncryptionGenerator(*config, alg) + return c.resendUserEmailCodeWithGenerator(ctx, userID, gen, returnCode, urlTmpl) } // changeUserEmailWithGenerator set a user's email address. // 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 and should be a validated Go template, if used. -func (c *Commands) changeUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*domain.Email, error) { - cmd, err := c.changeUserEmailWithGeneratorEvents(ctx, userID, resourceOwner, email, gen, returnCode, urlTmpl) +func (c *Commands) changeUserEmailWithGenerator(ctx context.Context, userID, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*domain.Email, error) { + cmd, err := c.changeUserEmailWithGeneratorEvents(ctx, userID, email, gen, returnCode, urlTmpl) if err != nil { return nil, err } return cmd.Push(ctx) } -func (c *Commands) changeUserEmailWithGeneratorEvents(ctx context.Context, userID, resourceOwner, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*UserEmailEvents, error) { - cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner) +func (c *Commands) resendUserEmailCodeWithGenerator(ctx context.Context, userID string, gen crypto.Generator, returnCode bool, urlTmpl string) (*domain.Email, error) { + cmd, err := c.resendUserEmailCodeWithGeneratorEvents(ctx, userID, gen, returnCode, urlTmpl) + if err != nil { + return nil, err + } + return cmd.Push(ctx) +} + +func (c *Commands) changeUserEmailWithGeneratorEvents(ctx context.Context, userID, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*UserEmailEvents, error) { + cmd, err := c.NewUserEmailEvents(ctx, userID) if err != nil { return nil, err } @@ -93,17 +132,36 @@ func (c *Commands) changeUserEmailWithGeneratorEvents(ctx context.Context, userI return cmd, nil } -func (c *Commands) VerifyUserEmail(ctx context.Context, userID, resourceOwner, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) { +func (c *Commands) resendUserEmailCodeWithGeneratorEvents(ctx context.Context, userID string, gen crypto.Generator, returnCode bool, urlTmpl string) (*UserEmailEvents, error) { + cmd, err := c.NewUserEmailEvents(ctx, userID) + if err != nil { + return nil, err + } + if authz.GetCtxData(ctx).UserID != userID { + if err = c.checkPermission(ctx, domain.PermissionUserWrite, cmd.aggregate.ResourceOwner, userID); err != nil { + return nil, err + } + } + if cmd.model.Code == nil { + return nil, zerrors.ThrowPreconditionFailed(err, "EMAIL-5w5ilin4yt", "Errors.User.Code.Empty") + } + if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil { + return nil, err + } + return cmd, nil +} + +func (c *Commands) VerifyUserEmail(ctx context.Context, userID, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) { config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode) if err != nil { return nil, err } gen := crypto.NewEncryptionGenerator(*config, alg) - return c.verifyUserEmailWithGenerator(ctx, userID, resourceOwner, code, gen) + return c.verifyUserEmailWithGenerator(ctx, userID, code, gen) } -func (c *Commands) verifyUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, code string, gen crypto.Generator) (*domain.ObjectDetails, error) { - cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner) +func (c *Commands) verifyUserEmailWithGenerator(ctx context.Context, userID, code string, gen crypto.Generator) (*domain.ObjectDetails, error) { + cmd, err := c.NewUserEmailEvents(ctx, userID) if err != nil { return nil, err } @@ -131,12 +189,12 @@ type UserEmailEvents struct { // 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) { +func (c *Commands) NewUserEmailEvents(ctx context.Context, userID string) (*UserEmailEvents, error) { if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing") } - model, err := c.emailWriteModel(ctx, userID, resourceOwner) + model, err := c.emailWriteModel(ctx, userID, "") if err != nil { return nil, err } diff --git a/internal/command/user_v2_email_test.go b/internal/command/user_v2_email_test.go index 262e672d00..c26d0c7493 100644 --- a/internal/command/user_v2_email_test.go +++ b/internal/command/user_v2_email_test.go @@ -26,9 +26,8 @@ func TestCommands_ChangeUserEmail(t *testing.T) { checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - email string + userID string + email string } tests := []struct { name string @@ -70,9 +69,8 @@ func TestCommands_ChangeUserEmail(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "", + userID: "user1", + email: "", }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -110,9 +108,8 @@ func TestCommands_ChangeUserEmail(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "", + userID: "user1", + email: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"), }, @@ -150,9 +147,8 @@ func TestCommands_ChangeUserEmail(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email@test.ch", + userID: "user1", + email: "email@test.ch", }, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged"), }, @@ -163,7 +159,7 @@ func TestCommands_ChangeUserEmail(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - _, err := c.ChangeUserEmail(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + _, err := c.ChangeUserEmail(context.Background(), tt.args.userID, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) require.ErrorIs(t, err, tt.wantErr) // successful cases are tested in TestCommands_changeUserEmailWithGenerator }) @@ -176,10 +172,9 @@ func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) { checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - email string - urlTmpl string + userID string + email string + urlTmpl string } tests := []struct { name string @@ -193,10 +188,9 @@ func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) { eventstore: eventstoreExpect(t), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email-changed@test.ch", - urlTmpl: "{{", + userID: "user1", + email: "email-changed@test.ch", + urlTmpl: "{{", }, wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"), }, @@ -234,10 +228,9 @@ func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email@test.ch", - urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + userID: "user1", + email: "email@test.ch", + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -275,10 +268,9 @@ func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email@test.ch", - urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + userID: "user1", + email: "email@test.ch", + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", }, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged"), }, @@ -289,7 +281,7 @@ func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - _, err := c.ChangeUserEmailURLTemplate(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t)), tt.args.urlTmpl) + _, err := c.ChangeUserEmailURLTemplate(context.Background(), tt.args.userID, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t)), tt.args.urlTmpl) require.ErrorIs(t, err, tt.wantErr) // successful cases are tested in TestCommands_changeUserEmailWithGenerator }) @@ -302,9 +294,8 @@ func TestCommands_ChangeUserEmailReturnCode(t *testing.T) { checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - email string + userID string + email string } tests := []struct { name string @@ -346,9 +337,8 @@ func TestCommands_ChangeUserEmailReturnCode(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email@test.ch", + userID: "user1", + email: "email@test.ch", }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -386,9 +376,8 @@ func TestCommands_ChangeUserEmailReturnCode(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "", + userID: "user1", + email: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"), }, @@ -399,22 +388,386 @@ func TestCommands_ChangeUserEmailReturnCode(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - _, err := c.ChangeUserEmailReturnCode(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + _, err := c.ChangeUserEmailReturnCode(context.Background(), tt.args.userID, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) require.ErrorIs(t, err, tt.wantErr) // successful cases are tested in TestCommands_changeUserEmailWithGenerator }) } } +func TestCommands_ResendUserEmailCode(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + userID string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "no code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "EMAIL-5w5ilin4yt", "Errors.User.Code.Empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + _, err := c.ResendUserEmailCode(context.Background(), tt.args.userID, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_changeUserEmailWithGenerator + }) + } +} + +func TestCommands_ResendUserEmailCodeURLTemplate(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + userID string + urlTmpl string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "invalid template", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + userID: "user1", + urlTmpl: "{{", + }, + wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"), + }, + { + name: "permission missing", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "no code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "EMAIL-5w5ilin4yt", "Errors.User.Code.Empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + _, err := c.ResendUserEmailCodeURLTemplate(context.Background(), tt.args.userID, crypto.CreateMockEncryptionAlg(gomock.NewController(t)), tt.args.urlTmpl) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_resendUserEmailCodeWithGenerator + }) + } +} + +func TestCommands_ResendUserEmailReturnCode(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + userID string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "missing code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "EMAIL-5w5ilin4yt", "Errors.User.Code.Empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + _, err := c.ResendUserEmailReturnCode(context.Background(), tt.args.userID, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_resendUserEmailCodeWithGenerator + }) + } +} + func TestCommands_ChangeUserEmailVerified(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - email string + userID string + email string } tests := []struct { name string @@ -430,9 +783,8 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "", - resourceOwner: "org1", - email: "email@test.ch", + userID: "", + email: "email@test.ch", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), }, @@ -461,9 +813,8 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email-changed@test.ch", + userID: "user1", + email: "email-changed@test.ch", }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -492,9 +843,8 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "", + userID: "user1", + email: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"), }, @@ -532,9 +882,8 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email-changed@test.ch", + userID: "user1", + email: "email-changed@test.ch", }, want: &domain.Email{ ObjectRoot: models.ObjectRoot{ @@ -552,7 +901,7 @@ func TestCommands_ChangeUserEmailVerified(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - got, err := c.ChangeUserEmailVerified(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email) + got, err := c.ChangeUserEmailVerified(context.Background(), tt.args.userID, tt.args.email) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) @@ -565,11 +914,10 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - email string - returnCode bool - urlTmpl string + userID string + email string + returnCode bool + urlTmpl string } tests := []struct { name string @@ -584,11 +932,10 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { eventstore: eventstoreExpect(t), }, args: args{ - userID: "", - resourceOwner: "org1", - email: "email@test.ch", - returnCode: false, - urlTmpl: "", + userID: "", + email: "email@test.ch", + returnCode: false, + urlTmpl: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), }, @@ -617,11 +964,10 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email@test.ch", - returnCode: false, - urlTmpl: "", + userID: "user1", + email: "email@test.ch", + returnCode: false, + urlTmpl: "", }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -650,11 +996,10 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "", - returnCode: false, - urlTmpl: "", + userID: "user1", + email: "", + returnCode: false, + urlTmpl: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"), }, @@ -683,11 +1028,10 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email@test.ch", - returnCode: false, - urlTmpl: "", + userID: "user1", + email: "email@test.ch", + returnCode: false, + urlTmpl: "", }, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged"), }, @@ -733,11 +1077,10 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email-changed@test.ch", - returnCode: false, - urlTmpl: "", + userID: "user1", + email: "email-changed@test.ch", + returnCode: false, + urlTmpl: "", }, want: &domain.Email{ ObjectRoot: models.ObjectRoot{ @@ -790,11 +1133,10 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email-changed@test.ch", - returnCode: true, - urlTmpl: "", + userID: "user1", + email: "email-changed@test.ch", + returnCode: true, + urlTmpl: "", }, want: &domain.Email{ ObjectRoot: models.ObjectRoot{ @@ -848,11 +1190,10 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - email: "email-changed@test.ch", - returnCode: false, - urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + userID: "user1", + email: "email-changed@test.ch", + returnCode: false, + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", }, want: &domain.Email{ ObjectRoot: models.ObjectRoot{ @@ -870,9 +1211,320 @@ func TestCommands_changeUserEmailWithGenerator(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - got, err := c.changeUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email, GetMockSecretGenerator(t), tt.args.returnCode, tt.args.urlTmpl) + got, err := c.changeUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.email, GetMockSecretGenerator(t), tt.args.returnCode, tt.args.urlTmpl) + require.ErrorIs(t, tt.wantErr, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCommands_resendUserEmailCodeWithGeneratorEvents(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + userID string + returnCode bool + urlTmpl string + } + tests := []struct { + name string + fields fields + args args + want *domain.Email + wantErr error + }{ + { + name: "missing user", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + userID: "", + returnCode: false, + urlTmpl: "", + }, + wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), + }, + { + name: "resend code, missing code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + returnCode: false, + urlTmpl: "", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "EMAIL-5w5ilin4yt", "Errors.User.Code.Empty"), + }, + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + returnCode: false, + urlTmpl: "", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "resend code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + expectPush( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + returnCode: false, + urlTmpl: "", + }, + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email@test.ch", + IsEmailVerified: false, + }, + }, + { + name: "resend code, return code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + expectPush( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + returnCode: true, + urlTmpl: "", + }, + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email@test.ch", + IsEmailVerified: false, + PlainCode: gu.Ptr("a"), + }, + }, + { + name: "resend code, URL template", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + expectPush( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + returnCode: false, + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email@test.ch", + IsEmailVerified: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + got, err := c.resendUserEmailCodeWithGenerator(context.Background(), tt.args.userID, GetMockSecretGenerator(t), tt.args.returnCode, tt.args.urlTmpl) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, got, tt.want) + assert.Equal(t, tt.want, got) }) } } @@ -882,9 +1534,8 @@ func TestCommands_VerifyUserEmail(t *testing.T) { eventstore *eventstore.Eventstore } type args struct { - userID string - resourceOwner string - code string + userID string + code string } tests := []struct { name string @@ -909,9 +1560,8 @@ func TestCommands_VerifyUserEmail(t *testing.T) { ), }, args: args{ - userID: "", - resourceOwner: "org1", - code: "a", + userID: "", + code: "a", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), }, @@ -948,9 +1598,8 @@ func TestCommands_VerifyUserEmail(t *testing.T) { ), }, args: args{ - userID: "user1", - resourceOwner: "org1", - code: "", + userID: "user1", + code: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty"), }, @@ -1005,9 +1654,8 @@ func TestCommands_VerifyUserEmail(t *testing.T) { ), }, args: args{ - userID: "user1", - resourceOwner: "org1", - code: "wrong", + userID: "user1", + code: "wrong", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-eis9R", "Errors.User.Code.Invalid"), }, @@ -1017,7 +1665,7 @@ func TestCommands_VerifyUserEmail(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore, } - _, err := c.VerifyUserEmail(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.code, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + _, err := c.VerifyUserEmail(context.Background(), tt.args.userID, tt.args.code, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) require.ErrorIs(t, err, tt.wantErr) // successful cases are tested in TestCommands_verifyUserEmailWithGenerator }) @@ -1029,9 +1677,8 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { eventstore *eventstore.Eventstore } type args struct { - userID string - resourceOwner string - code string + userID string + code string } tests := []struct { name string @@ -1046,9 +1693,8 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { eventstore: eventstoreExpect(t), }, args: args{ - userID: "", - resourceOwner: "org1", - code: "a", + userID: "", + code: "a", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), }, @@ -1076,9 +1722,8 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { ), }, args: args{ - userID: "user1", - resourceOwner: "org1", - code: "", + userID: "user1", + code: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty"), }, @@ -1124,9 +1769,8 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { ), }, args: args{ - userID: "user1", - resourceOwner: "org1", - code: "wrong", + userID: "user1", + code: "wrong", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-eis9R", "Errors.User.Code.Invalid"), }, @@ -1172,9 +1816,8 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { ), }, args: args{ - userID: "user1", - resourceOwner: "org1", - code: "a", + userID: "user1", + code: "a", }, want: &domain.ObjectDetails{ ResourceOwner: "org1", @@ -1186,7 +1829,7 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore, } - got, err := c.verifyUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.code, GetMockSecretGenerator(t)) + got, err := c.verifyUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.code, GetMockSecretGenerator(t)) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, got, tt.want) }) @@ -1198,8 +1841,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) { eventstore *eventstore.Eventstore } type args struct { - userID string - resourceOwner string + userID string } tests := []struct { name string @@ -1213,8 +1855,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) { eventstore: eventstoreExpect(t), }, args: args{ - userID: "", - resourceOwner: "org1", + userID: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), }, @@ -1224,8 +1865,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter()), }, args: args{ - userID: "user1", - resourceOwner: "org1", + userID: "user1", }, wantErr: zerrors.ThrowNotFound(nil, "COMMAND-ieJ2e", "Errors.User.Email.NotFound"), }, @@ -1259,8 +1899,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) { ), }, args: args{ - userID: "user1", - resourceOwner: "org1", + userID: "user1", }, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-uz0Uu", "Errors.User.NotInitialised"), }, @@ -1270,7 +1909,7 @@ func TestCommands_NewUserEmailEvents(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore, } - _, err := c.NewUserEmailEvents(context.Background(), tt.args.userID, tt.args.resourceOwner) + _, err := c.NewUserEmailEvents(context.Background(), tt.args.userID) require.ErrorIs(t, err, tt.wantErr) // successful cases are tested in TestCommands_changeUserEmailWithGenerator }) diff --git a/internal/command/user_v2_phone.go b/internal/command/user_v2_phone.go index e501b3e3f1..d2582b17c8 100644 --- a/internal/command/user_v2_phone.go +++ b/internal/command/user_v2_phone.go @@ -15,20 +15,20 @@ import ( // ChangeUserPhone sets a user's phone number, generates a code // and triggers a notification sms. -func (c *Commands) ChangeUserPhone(ctx context.Context, userID, resourceOwner, phone string, alg crypto.EncryptionAlgorithm) (*domain.Phone, error) { - return c.changeUserPhoneWithCode(ctx, userID, resourceOwner, phone, alg, false) +func (c *Commands) ChangeUserPhone(ctx context.Context, userID, phone string, alg crypto.EncryptionAlgorithm) (*domain.Phone, error) { + return c.changeUserPhoneWithCode(ctx, userID, phone, alg, false) } // ChangeUserPhoneReturnCode sets a user's phone number, generates a code and does not send a notification sms. // The generated plain text code will be set in the returned Phone object. -func (c *Commands) ChangeUserPhoneReturnCode(ctx context.Context, userID, resourceOwner, phone string, alg crypto.EncryptionAlgorithm) (*domain.Phone, error) { - return c.changeUserPhoneWithCode(ctx, userID, resourceOwner, phone, alg, true) +func (c *Commands) ChangeUserPhoneReturnCode(ctx context.Context, userID, phone string, alg crypto.EncryptionAlgorithm) (*domain.Phone, error) { + return c.changeUserPhoneWithCode(ctx, userID, phone, alg, true) } // ChangeUserPhoneVerified sets a user's phone number and marks it is verified. // No code is generated and no confirmation sms is send. -func (c *Commands) ChangeUserPhoneVerified(ctx context.Context, userID, resourceOwner, phone string) (*domain.Phone, error) { - cmd, err := c.NewUserPhoneEvents(ctx, userID, resourceOwner) +func (c *Commands) ChangeUserPhoneVerified(ctx context.Context, userID, phone string) (*domain.Phone, error) { + cmd, err := c.NewUserPhoneEvents(ctx, userID) if err != nil { return nil, err } @@ -42,20 +42,41 @@ func (c *Commands) ChangeUserPhoneVerified(ctx context.Context, userID, resource return cmd.Push(ctx) } -func (c *Commands) changeUserPhoneWithCode(ctx context.Context, userID, resourceOwner, phone string, alg crypto.EncryptionAlgorithm, returnCode bool) (*domain.Phone, error) { +// ResendUserPhoneCode generates a code +// and triggers a notification sms. +func (c *Commands) ResendUserPhoneCode(ctx context.Context, userID string, alg crypto.EncryptionAlgorithm) (*domain.Phone, error) { + return c.resendUserPhoneCode(ctx, userID, alg, false) +} + +// ResendUserPhoneCodeReturnCode generates a code and does not send a notification sms. +// The generated plain text code will be set in the returned Phone object. +func (c *Commands) ResendUserPhoneCodeReturnCode(ctx context.Context, userID string, alg crypto.EncryptionAlgorithm) (*domain.Phone, error) { + return c.resendUserPhoneCode(ctx, userID, alg, true) +} + +func (c *Commands) changeUserPhoneWithCode(ctx context.Context, userID, phone string, alg crypto.EncryptionAlgorithm, returnCode bool) (*domain.Phone, error) { config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode) if err != nil { return nil, err } gen := crypto.NewEncryptionGenerator(*config, alg) - return c.changeUserPhoneWithGenerator(ctx, userID, resourceOwner, phone, gen, returnCode) + return c.changeUserPhoneWithGenerator(ctx, userID, phone, gen, returnCode) +} + +func (c *Commands) resendUserPhoneCode(ctx context.Context, userID string, alg crypto.EncryptionAlgorithm, returnCode bool) (*domain.Phone, error) { + config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode) //nolint:staticcheck + if err != nil { + return nil, err + } + gen := crypto.NewEncryptionGenerator(*config, alg) + return c.resendUserPhoneCodeWithGenerator(ctx, userID, gen, returnCode) } // changeUserPhoneWithGenerator set a user's phone number. // 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 sms will be send to the user. -func (c *Commands) changeUserPhoneWithGenerator(ctx context.Context, userID, resourceOwner, phone string, gen crypto.Generator, returnCode bool) (*domain.Phone, error) { - cmd, err := c.NewUserPhoneEvents(ctx, userID, resourceOwner) +func (c *Commands) changeUserPhoneWithGenerator(ctx context.Context, userID, phone string, gen crypto.Generator, returnCode bool) (*domain.Phone, error) { + cmd, err := c.NewUserPhoneEvents(ctx, userID) if err != nil { return nil, err } @@ -73,17 +94,39 @@ func (c *Commands) changeUserPhoneWithGenerator(ctx context.Context, userID, res return cmd.Push(ctx) } -func (c *Commands) VerifyUserPhone(ctx context.Context, userID, resourceOwner, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) { +// resendUserPhoneCodeWithGenerator generates a new code. +// 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 sms will be send to the user. +func (c *Commands) resendUserPhoneCodeWithGenerator(ctx context.Context, userID string, gen crypto.Generator, returnCode bool) (*domain.Phone, error) { + cmd, err := c.NewUserPhoneEvents(ctx, userID) + if err != nil { + return nil, err + } + if authz.GetCtxData(ctx).UserID != userID { + if err = c.checkPermission(ctx, domain.PermissionUserWrite, cmd.aggregate.ResourceOwner, userID); err != nil { + return nil, err + } + } + if cmd.model.Code == nil { + return nil, zerrors.ThrowPreconditionFailed(err, "PHONE-5xrra88eq8", "Errors.User.Code.Empty") + } + if err = cmd.AddGeneratedCode(ctx, gen, returnCode); err != nil { + return nil, err + } + return cmd.Push(ctx) +} + +func (c *Commands) VerifyUserPhone(ctx context.Context, userID, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) { config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode) if err != nil { return nil, err } gen := crypto.NewEncryptionGenerator(*config, alg) - return c.verifyUserPhoneWithGenerator(ctx, userID, resourceOwner, code, gen) + return c.verifyUserPhoneWithGenerator(ctx, userID, code, gen) } -func (c *Commands) verifyUserPhoneWithGenerator(ctx context.Context, userID, resourceOwner, code string, gen crypto.Generator) (*domain.ObjectDetails, error) { - cmd, err := c.NewUserPhoneEvents(ctx, userID, resourceOwner) +func (c *Commands) verifyUserPhoneWithGenerator(ctx context.Context, userID, code string, gen crypto.Generator) (*domain.ObjectDetails, error) { + cmd, err := c.NewUserPhoneEvents(ctx, userID) if err != nil { return nil, err } @@ -111,12 +154,12 @@ type UserPhoneEvents struct { // NewUserPhoneEvents constructs a UserPhoneEvents with a Human Phone 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) NewUserPhoneEvents(ctx context.Context, userID, resourceOwner string) (*UserPhoneEvents, error) { +func (c *Commands) NewUserPhoneEvents(ctx context.Context, userID string) (*UserPhoneEvents, error) { if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xP292j", "Errors.User.Phone.IDMissing") } - model, err := c.phoneWriteModelByID(ctx, userID, resourceOwner) + model, err := c.phoneWriteModelByID(ctx, userID, "") if err != nil { return nil, err } diff --git a/internal/command/user_v2_phone_test.go b/internal/command/user_v2_phone_test.go index fe0c9f636f..d64960efd9 100644 --- a/internal/command/user_v2_phone_test.go +++ b/internal/command/user_v2_phone_test.go @@ -26,9 +26,8 @@ func TestCommands_ChangeUserPhone(t *testing.T) { checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - phone string + userID string + phone string } tests := []struct { name string @@ -74,9 +73,8 @@ func TestCommands_ChangeUserPhone(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "", + userID: "user1", + phone: "", }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -118,9 +116,8 @@ func TestCommands_ChangeUserPhone(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "", + userID: "user1", + phone: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"), }, @@ -162,9 +159,8 @@ func TestCommands_ChangeUserPhone(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "+41791234567", + userID: "user1", + phone: "+41791234567", }, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Phone.NotChanged"), }, @@ -175,7 +171,7 @@ func TestCommands_ChangeUserPhone(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - _, err := c.ChangeUserPhone(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + _, err := c.ChangeUserPhone(context.Background(), tt.args.userID, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) require.ErrorIs(t, err, tt.wantErr) // successful cases are tested in TestCommands_changeUserPhoneWithGenerator }) @@ -188,9 +184,8 @@ func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - phone string + userID string + phone string } tests := []struct { name string @@ -236,9 +231,8 @@ func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "+41791234567", + userID: "user1", + phone: "+41791234567", }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -280,9 +274,8 @@ func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "", + userID: "user1", + phone: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"), }, @@ -293,22 +286,245 @@ func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - _, err := c.ChangeUserPhoneReturnCode(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + _, err := c.ChangeUserPhoneReturnCode(context.Background(), tt.args.userID, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) require.ErrorIs(t, err, tt.wantErr) // successful cases are tested in TestCommands_changeUserPhoneWithGenerator }) } } +func TestCommands_ResendUserPhoneCode(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + userID string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyPhoneCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "no code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyPhoneCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "PHONE-5xrra88eq8", "Errors.User.Code.Empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + _, err := c.ResendUserPhoneCode(context.Background(), tt.args.userID, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_resendUserPhoneCodeWithGenerator + }) + } +} + +func TestCommands_ResendUserPhoneCodeReturnCode(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + userID string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyPhoneCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "no code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "PHONE-5xrra88eq8", "Errors.User.Code.Empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + _, err := c.ResendUserPhoneCodeReturnCode(context.Background(), tt.args.userID, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_resendUserPhoneCodeWithGenerator + }) + } +} + func TestCommands_ChangeUserPhoneVerified(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - phone string + userID string + phone string } tests := []struct { name string @@ -324,9 +540,8 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "", - resourceOwner: "org1", - phone: "+41791234567", + userID: "", + phone: "+41791234567", }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-xP292j", "Errors.User.Phone.IDMissing"), }, @@ -359,9 +574,8 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "+41791234567", + userID: "user1", + phone: "+41791234567", }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -394,9 +608,8 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "", + userID: "user1", + phone: "", }, wantErr: zerrors.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"), }, @@ -438,9 +651,8 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "+41791234568", + userID: "user1", + phone: "+41791234568", }, want: &domain.Phone{ ObjectRoot: models.ObjectRoot{ @@ -458,7 +670,7 @@ func TestCommands_ChangeUserPhoneVerified(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - got, err := c.ChangeUserPhoneVerified(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone) + got, err := c.ChangeUserPhoneVerified(context.Background(), tt.args.userID, tt.args.phone) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, got, tt.want) }) @@ -471,10 +683,9 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { checkPermission domain.PermissionCheck } type args struct { - userID string - resourceOwner string - phone string - returnCode bool + userID string + phone string + returnCode bool } tests := []struct { name string @@ -489,10 +700,9 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { eventstore: eventstoreExpect(t), }, args: args{ - userID: "", - resourceOwner: "org1", - phone: "+41791234567", - returnCode: false, + userID: "", + phone: "+41791234567", + returnCode: false, }, wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-xP292j", "Errors.User.Phone.IDMissing"), }, @@ -525,10 +735,9 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "+41791234567", - returnCode: false, + userID: "user1", + phone: "+41791234567", + returnCode: false, }, wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, @@ -561,10 +770,9 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "", - returnCode: false, + userID: "user1", + phone: "", + returnCode: false, }, wantErr: zerrors.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"), }, @@ -597,10 +805,9 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "+41791234567", - returnCode: false, + userID: "user1", + phone: "+41791234567", + returnCode: false, }, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Phone.NotChanged"), }, @@ -650,10 +857,9 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "+41791234568", - returnCode: false, + userID: "user1", + phone: "+41791234568", + returnCode: false, }, want: &domain.Phone{ ObjectRoot: models.ObjectRoot{ @@ -710,10 +916,9 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - userID: "user1", - resourceOwner: "org1", - phone: "+41791234568", - returnCode: true, + userID: "user1", + phone: "+41791234568", + returnCode: true, }, want: &domain.Phone{ ObjectRoot: models.ObjectRoot{ @@ -732,7 +937,251 @@ func TestCommands_changeUserPhoneWithGenerator(t *testing.T) { eventstore: tt.fields.eventstore, checkPermission: tt.fields.checkPermission, } - got, err := c.changeUserPhoneWithGenerator(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone, GetMockSecretGenerator(t), tt.args.returnCode) + got, err := c.changeUserPhoneWithGenerator(context.Background(), tt.args.userID, tt.args.phone, GetMockSecretGenerator(t), tt.args.returnCode) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, got, tt.want) + }) + } +} + +func TestCommands_resendUserPhoneCodeWithGenerator(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + userID string + returnCode bool + } + tests := []struct { + name string + fields fields + args args + want *domain.Phone + wantErr error + }{ + { + name: "missing user", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + userID: "", + returnCode: false, + }, + wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-xP292j", "Errors.User.Phone.IDMissing"), + }, + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + returnCode: false, + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "no code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + returnCode: false, + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "PHONE-5xrra88eq8", "Errors.User.Code.Empty"), + }, + { + name: "resend", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + true, + ), + ), + ), + expectPush( + user.NewHumanPhoneCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + returnCode: false, + }, + want: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + PhoneNumber: "+41791234567", + IsPhoneVerified: false, + }, + }, + { + name: "return code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + event.AddPhoneData("+41791234567") + return event + }(), + ), + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + true, + ), + ), + ), + expectPush( + user.NewHumanPhoneCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + returnCode: true, + }, + want: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + PhoneNumber: "+41791234567", + IsPhoneVerified: false, + PlainCode: gu.Ptr("a"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + got, err := c.resendUserPhoneCodeWithGenerator(context.Background(), tt.args.userID, GetMockSecretGenerator(t), tt.args.returnCode) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, got, tt.want) }) diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index cb119c455d..9715969807 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -130,6 +130,7 @@ Errors: NotFound: Кодът не е намерен Expired: Кодът е изтекъл GeneratorAlgNotSupported: Неподдържан генераторен алгоритъм + Invalid: кодът е невалиден Password: NotFound: Паролата не е намерена Empty: Паролата е празна diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index a601509d20..cf23b93d23 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -127,6 +127,7 @@ Errors: NotFound: Kód nenalezen Expired: Kód vypršel GeneratorAlgNotSupported: Nepodporovaný algoritmus generátoru + Invalid: Kód je neplatný Password: NotFound: Heslo nenalezeno Empty: Heslo je prázdné diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 93825e9338..80ba38029e 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -128,6 +128,7 @@ Errors: NotFound: Code konnte nicht gefunden werden Expired: Code ist abgelaufen GeneratorAlgNotSupported: Generator Algorithmus wird nicht unterstützt + Invalid: Code ist nicht gültig Password: NotFound: Password nicht gefunden Empty: Passwort ist leer diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 0abdc71d66..88146f7260 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -128,6 +128,7 @@ Errors: NotFound: Code not found Expired: Code is expired GeneratorAlgNotSupported: Unsupported generator algorithm + Invalid: Code is invalid Password: NotFound: Password not found Empty: Password is empty diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 0ea1aecfaf..3c8cec1727 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -128,6 +128,7 @@ Errors: NotFound: Código no encontrado Expired: El código ha caducado GeneratorAlgNotSupported: Algoritmo generador no soportado + Invalid: El código no es válido Password: NotFound: Contraseña no encontrada Empty: La contraseña está vacía diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index eeee4608bb..934116ca58 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -128,6 +128,7 @@ Errors: NotFound: Code non trouvé Expired: Le code est expiré GeneratorAlgNotSupported: Algorithme de générateur non pris en charge + Invalid: Le code n'est pas valide Password: NotFound: Mot de passe non trouvé Empty: Le mot de passe est vide diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 198496cba4..c5447304e9 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -128,6 +128,7 @@ Errors: NotFound: Codice non trovato Expired: Il codice è scaduto GeneratorAlgNotSupported: L'algoritmo del generatore non è supportato + Invalid: Il codice non è valido Password: NotFound: Password non trovato Empty: La password è vuota diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 8dcc8a570d..88d75e39f3 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -120,6 +120,7 @@ Errors: NotFound: コードが見つかりません Expired: 有効期限切れのコードです GeneratorAlgNotSupported: サポートされていない生成アルゴリズムです + Invalid: コードが無効 Password: NotFound: パスワードが見つかりません Empty: パスワードは空です diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index ec1bebf56b..52137d78d5 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -128,6 +128,7 @@ Errors: NotFound: Code niet gevonden Expired: Code is verlopen GeneratorAlgNotSupported: Generator algoritme wordt niet ondersteund + Invalid: Code is ongeldig Password: NotFound: Wachtwoord niet gevonden Empty: Wachtwoord is leeg diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 70ce7fe753..ec8a36c673 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -128,6 +128,7 @@ Errors: NotFound: Kod nie znaleziony Expired: Kod jest przedawniony GeneratorAlgNotSupported: Nieobsługiwany algorytm generatora + Invalid: Kod jest nieprawidłowy Password: NotFound: Hasło nie znalezione Empty: Hasło jest puste diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index c00c4f7171..990f939b25 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -128,6 +128,7 @@ Errors: NotFound: Código não encontrado Expired: Código expirou GeneratorAlgNotSupported: Algoritmo do gerador não suportado + Invalid: Código é inválido Password: NotFound: Senha não encontrada Empty: Senha está vazia diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 74a925bdb8..c609cdef01 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -127,6 +127,7 @@ Errors: NotFound: Код не найден Expired: Срок действия кода истек GeneratorAlgNotSupported: Неподдерживаемый алгоритм генератора + Invalid: Код недействителен Password: NotFound: Пароль не найден Empty: Пароль пуст diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 54bb911e17..4d21f82f12 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -128,6 +128,7 @@ Errors: NotFound: 验证码不存在 Expired: 验证码已过期 GeneratorAlgNotSupported: 不支持的生成器算法 + Invalid: 代码无效 Password: NotFound: 未找到密码 Empty: 密码为空 diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 82821734cb..a13e50cfc7 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -231,6 +231,32 @@ service UserService { }; } + + // Resend code to verify user email + rpc ResendEmailCode (ResendEmailCodeRequest) returns (ResendEmailCodeResponse) { + option (google.api.http) = { + post: "/v2beta/users/{user_id}/email/resend" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Resend code to verify user email"; + description: "Resend code to verify user email." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + // Verify the email with the provided code rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse) { option (google.api.http) = { @@ -281,6 +307,30 @@ service UserService { }; } + rpc ResendPhoneCode (ResendPhoneCodeRequest) returns (ResendPhoneCodeResponse) { + option (google.api.http) = { + post: "/v2beta/users/{user_id}/phone/resend" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Resend code to verify user phone"; + description: "Resend code to verify user phone." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + // Verify the phone with the provided code rpc VerifyPhone (VerifyPhoneRequest) returns (VerifyPhoneResponse) { option (google.api.http) = { @@ -963,6 +1013,29 @@ message SetEmailResponse{ optional string verification_code = 2; } +message ResendEmailCodeRequest{ + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // if no verification is specified, an email is sent with the default url + oneof verification { + SendEmailVerificationCode send_code = 2; + ReturnEmailVerificationCode return_code = 3; + } +} + +message ResendEmailCodeResponse{ + zitadel.object.v2beta.Details details = 1; + // in case the verification was set to return_code, the code will be returned + optional string verification_code = 2; +} + message VerifyEmailRequest{ string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -1022,6 +1095,29 @@ message SetPhoneResponse{ optional string verification_code = 2; } +message ResendPhoneCodeRequest{ + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // if no verification is specified, an sms is sent + oneof verification { + SendPhoneVerificationCode send_code = 3; + ReturnPhoneVerificationCode return_code = 4; + } +} + +message ResendPhoneCodeResponse{ + zitadel.object.v2beta.Details details = 1; + // in case the verification was set to return_code, the code will be returned + optional string verification_code = 2; +} + message VerifyPhoneRequest{ string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200},