mirror of
https://github.com/zitadel/zitadel.git
synced 2025-05-17 03:38:21 +00:00
feat: user v2 phone verification (#6309)
* feat: add phone change and code verification for user v2 api * feat: add phone change and code verification for user v2 api * fix: add ignored phone.proto * fix: integration tests * Update proto/zitadel/user/v2alpha/user_service.proto * Update idp_template.go --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
parent
a1942ecdaa
commit
ef012d0081
61
internal/api/grpc/user/v2/phone.go
Normal file
61
internal/api/grpc/user/v2/phone.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
case *user.SetPhoneRequest_ReturnCode:
|
||||||
|
phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetPhone(), s.userCodeAlg)
|
||||||
|
case *user.SetPhoneRequest_IsVerified:
|
||||||
|
phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), resourceOwner, req.GetPhone())
|
||||||
|
case nil:
|
||||||
|
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), resourceOwner, req.GetPhone(), s.userCodeAlg)
|
||||||
|
default:
|
||||||
|
err = caos_errs.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user.SetPhoneResponse{
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.VerifyPhoneResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: details.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(details.EventDate),
|
||||||
|
ResourceOwner: details.ResourceOwner,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
171
internal/api/grpc/user/v2/phone_integration_test.go
Normal file
171
internal/api/grpc/user/v2/phone_integration_test.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_SetPhone(t *testing.T) {
|
||||||
|
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req *user.SetPhoneRequest
|
||||||
|
want *user.SetPhoneResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default verification",
|
||||||
|
req: &user.SetPhoneRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Phone: "+41791234568",
|
||||||
|
},
|
||||||
|
want: &user.SetPhoneResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: 1,
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "send verification",
|
||||||
|
req: &user.SetPhoneRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Phone: "+41791234569",
|
||||||
|
Verification: &user.SetPhoneRequest_SendCode{
|
||||||
|
SendCode: &user.SendPhoneVerificationCode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.SetPhoneResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: 1,
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return code",
|
||||||
|
req: &user.SetPhoneRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Phone: "+41791234566",
|
||||||
|
Verification: &user.SetPhoneRequest_ReturnCode{
|
||||||
|
ReturnCode: &user.ReturnPhoneVerificationCode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.SetPhoneResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: 1,
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
VerificationCode: gu.Ptr("xxx"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is verified true",
|
||||||
|
req: &user.SetPhoneRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Phone: "+41791234565",
|
||||||
|
Verification: &user.SetPhoneRequest_IsVerified{
|
||||||
|
IsVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.SetPhoneResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: 1,
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is verified false",
|
||||||
|
req: &user.SetPhoneRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Phone: "+41791234564",
|
||||||
|
Verification: &user.SetPhoneRequest_IsVerified{
|
||||||
|
IsVerified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Client.SetPhone(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 {
|
||||||
|
name string
|
||||||
|
req *user.VerifyPhoneRequest
|
||||||
|
want *user.VerifyPhoneResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "wrong code",
|
||||||
|
req: &user.VerifyPhoneRequest{
|
||||||
|
UserId: userResp.GetUserId(),
|
||||||
|
VerificationCode: "xxx",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong user",
|
||||||
|
req: &user.VerifyPhoneRequest{
|
||||||
|
UserId: "xxx",
|
||||||
|
VerificationCode: userResp.GetPhoneCode(),
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "verify user",
|
||||||
|
req: &user.VerifyPhoneRequest{
|
||||||
|
UserId: userResp.GetUserId(),
|
||||||
|
VerificationCode: userResp.GetPhoneCode(),
|
||||||
|
},
|
||||||
|
want: &user.VerifyPhoneResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: 1,
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Client.VerifyPhone(CTX, tt.req)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
integration.AssertDetails(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
|
|||||||
UserId: human.ID,
|
UserId: human.ID,
|
||||||
Details: object.DomainToDetailsPb(human.Details),
|
Details: object.DomainToDetailsPb(human.Details),
|
||||||
EmailCode: human.EmailCode,
|
EmailCode: human.EmailCode,
|
||||||
|
PhoneCode: human.PhoneCode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,9 +78,13 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
|
|||||||
ReturnCode: req.GetEmail().GetReturnCode() != nil,
|
ReturnCode: req.GetEmail().GetReturnCode() != nil,
|
||||||
URLTemplate: urlTemplate,
|
URLTemplate: urlTemplate,
|
||||||
},
|
},
|
||||||
|
Phone: command.Phone{
|
||||||
|
Number: domain.PhoneNumber(req.GetPhone().GetPhone()),
|
||||||
|
Verified: req.GetPhone().GetIsVerified(),
|
||||||
|
ReturnCode: req.GetPhone().GetReturnCode() != nil,
|
||||||
|
},
|
||||||
PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()),
|
PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()),
|
||||||
Gender: genderToDomain(req.GetProfile().GetGender()),
|
Gender: genderToDomain(req.GetProfile().GetGender()),
|
||||||
Phone: command.Phone{}, // TODO: add as soon as possible
|
|
||||||
Password: req.GetPassword().GetPassword(),
|
Password: req.GetPassword().GetPassword(),
|
||||||
EncodedPasswordHash: req.GetHashedPassword().GetHash(),
|
EncodedPasswordHash: req.GetHashedPassword().GetHash(),
|
||||||
PasswordChangeRequired: passwordChangeRequired,
|
PasswordChangeRequired: passwordChangeRequired,
|
||||||
|
@ -75,6 +75,7 @@ func TestServer_AddHumanUser(t *testing.T) {
|
|||||||
Gender: user.Gender_GENDER_DIVERSE.Enum(),
|
Gender: user.Gender_GENDER_DIVERSE.Enum(),
|
||||||
},
|
},
|
||||||
Email: &user.SetHumanEmail{},
|
Email: &user.SetHumanEmail{},
|
||||||
|
Phone: &user.SetHumanPhone{},
|
||||||
Metadata: []*user.SetMetadataEntry{
|
Metadata: []*user.SetMetadataEntry{
|
||||||
{
|
{
|
||||||
Key: "somekey",
|
Key: "somekey",
|
||||||
@ -97,7 +98,7 @@ func TestServer_AddHumanUser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "return verification code",
|
name: "return email verification code",
|
||||||
args: args{
|
args: args{
|
||||||
CTX,
|
CTX,
|
||||||
&user.AddHumanUserRequest{
|
&user.AddHumanUserRequest{
|
||||||
@ -187,6 +188,53 @@ func TestServer_AddHumanUser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "return phone verification code",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&user.AddHumanUserRequest{
|
||||||
|
Organisation: &object.Organisation{
|
||||||
|
Org: &object.Organisation_OrgId{
|
||||||
|
OrgId: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Profile: &user.SetHumanProfile{
|
||||||
|
FirstName: "Donald",
|
||||||
|
LastName: "Duck",
|
||||||
|
NickName: gu.Ptr("Dukkie"),
|
||||||
|
DisplayName: gu.Ptr("Donald Duck"),
|
||||||
|
PreferredLanguage: gu.Ptr("en"),
|
||||||
|
Gender: user.Gender_GENDER_DIVERSE.Enum(),
|
||||||
|
},
|
||||||
|
Email: &user.SetHumanEmail{},
|
||||||
|
Phone: &user.SetHumanPhone{
|
||||||
|
Phone: "+41791234567",
|
||||||
|
Verification: &user.SetHumanPhone_ReturnCode{
|
||||||
|
ReturnCode: &user.ReturnPhoneVerificationCode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: []*user.SetMetadataEntry{
|
||||||
|
{
|
||||||
|
Key: "somekey",
|
||||||
|
Value: []byte("somevalue"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PasswordType: &user.AddHumanUserRequest_Password{
|
||||||
|
Password: &user.Password{
|
||||||
|
Password: "DifficultPW666!",
|
||||||
|
ChangeRequired: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.AddHumanUserResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
PhoneCode: gu.Ptr("something"),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "custom template error",
|
name: "custom template error",
|
||||||
args: args{
|
args: args{
|
||||||
|
@ -11,6 +11,9 @@ import (
|
|||||||
type Phone struct {
|
type Phone struct {
|
||||||
Number domain.PhoneNumber
|
Number domain.PhoneNumber
|
||||||
Verified bool
|
Verified bool
|
||||||
|
|
||||||
|
// ReturnCode is used if the Verified field is false
|
||||||
|
ReturnCode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCode, error) {
|
func (c *Commands) newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCode, error) {
|
||||||
|
@ -66,6 +66,9 @@ type AddHuman struct {
|
|||||||
|
|
||||||
// EmailCode is set by the command
|
// EmailCode is set by the command
|
||||||
EmailCode *string
|
EmailCode *string
|
||||||
|
|
||||||
|
// PhoneCode is set by the command
|
||||||
|
PhoneCode *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddLink struct {
|
type AddLink struct {
|
||||||
@ -258,7 +261,6 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
|
|||||||
if human.Email.Verified {
|
if human.Email.Verified {
|
||||||
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
|
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if allowInitMail, used for v1 api (system, admin, mgmt, auth):
|
// if allowInitMail, used for v1 api (system, admin, mgmt, auth):
|
||||||
// add init code if
|
// add init code if
|
||||||
// email not verified or
|
// email not verified or
|
||||||
@ -302,7 +304,10 @@ func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry)), nil
|
if human.Phone.ReturnCode {
|
||||||
|
human.PhoneCode = &phoneCode.Plain
|
||||||
|
}
|
||||||
|
return append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry, human.Phone.ReturnCode)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, human *AddHuman, orgID string) (err error) {
|
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, human *AddHuman, orgID string) (err error) {
|
||||||
|
@ -1020,6 +1020,92 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantID: "user1",
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
name: "add human (with return code), ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("$plain$x$password", false, "+41711234567"),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanPhoneCodeAddedEventV2(
|
||||||
|
context.Background(),
|
||||||
|
&userAgg.Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("phoneCode"),
|
||||||
|
},
|
||||||
|
1*time.Hour,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||||
|
userPasswordHasher: mockPasswordHasher("x"),
|
||||||
|
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
newCode: mockCode("phoneCode", time.Hour),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
Verified: true,
|
||||||
|
},
|
||||||
|
Phone: Phone{
|
||||||
|
Number: "+41711234567",
|
||||||
|
ReturnCode: true,
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
wantID: "user1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "add human with metadata, ok",
|
name: "add human with metadata, ok",
|
||||||
|
200
internal/command/user_v2_phone.go
Normal file
200
internal/command/user_v2_phone.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = c.checkPermission(ctx, domain.PermissionUserWrite, cmd.aggregate.ResourceOwner, userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = cmd.Change(ctx, domain.PhoneNumber(phone)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cmd.SetVerified(ctx)
|
||||||
|
return cmd.Push(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) changeUserPhoneWithCode(ctx context.Context, userID, resourceOwner, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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 err = cmd.Change(ctx, domain.PhoneNumber(phone)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = cmd.AddGeneratedCode(ctx, gen, returnCode); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd.Push(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) VerifyUserPhone(ctx context.Context, userID, resourceOwner, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) verifyUserPhoneWithGenerator(ctx context.Context, userID, resourceOwner, code string, gen crypto.Generator) (*domain.ObjectDetails, error) {
|
||||||
|
cmd, err := c.NewUserPhoneEvents(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = cmd.VerifyCode(ctx, code, gen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = cmd.Push(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return writeModelToObjectDetails(&cmd.model.WriteModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPhoneEvents allows step-by-step additions of events,
|
||||||
|
// operating on the Human Phone Model.
|
||||||
|
type UserPhoneEvents struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
aggregate *eventstore.Aggregate
|
||||||
|
events []eventstore.Command
|
||||||
|
model *HumanPhoneWriteModel
|
||||||
|
|
||||||
|
plainCode *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
if userID == "" {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-xP292j", "Errors.User.Phone.IDMissing")
|
||||||
|
}
|
||||||
|
|
||||||
|
model, err := c.phoneWriteModelByID(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if model.UserState == domain.UserStateUnspecified || model.UserState == domain.UserStateDeleted {
|
||||||
|
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-ieJ2e", "Errors.User.Phone.NotFound")
|
||||||
|
}
|
||||||
|
if model.UserState == domain.UserStateInitial {
|
||||||
|
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-uz0Uu", "Errors.User.NotInitialised")
|
||||||
|
}
|
||||||
|
return &UserPhoneEvents{
|
||||||
|
eventstore: c.eventstore,
|
||||||
|
aggregate: UserAggregateFromWriteModel(&model.WriteModel),
|
||||||
|
model: model,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change sets a new phone number.
|
||||||
|
// The generated event unsets any previously generated code and verified flag.
|
||||||
|
func (c *UserPhoneEvents) Change(ctx context.Context, phone domain.PhoneNumber) error {
|
||||||
|
phone, err := phone.Normalize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
event, hasChanged := c.model.NewChangedEvent(ctx, c.aggregate, phone)
|
||||||
|
if !hasChanged {
|
||||||
|
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Phone.NotChanged")
|
||||||
|
}
|
||||||
|
c.events = append(c.events, event)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVerified sets the phone number to verified.
|
||||||
|
func (c *UserPhoneEvents) SetVerified(ctx context.Context) {
|
||||||
|
c.events = append(c.events, user.NewHumanPhoneVerifiedEvent(ctx, c.aggregate))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddGeneratedCode generates a new encrypted code and sets it to the phone number.
|
||||||
|
// When returnCode a plain text of the code will be returned from Push.
|
||||||
|
func (c *UserPhoneEvents) AddGeneratedCode(ctx context.Context, gen crypto.Generator, returnCode bool) error {
|
||||||
|
value, plain, err := crypto.NewCode(gen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.events = append(c.events, user.NewHumanPhoneCodeAddedEventV2(ctx, c.aggregate, value, gen.Expiry(), returnCode))
|
||||||
|
if returnCode {
|
||||||
|
c.plainCode = &plain
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserPhoneEvents) VerifyCode(ctx context.Context, code string, gen crypto.Generator) error {
|
||||||
|
if code == "" {
|
||||||
|
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := crypto.VerifyCode(c.model.CodeCreationDate, c.model.CodeExpiry, c.model.Code, code, gen)
|
||||||
|
if err == nil {
|
||||||
|
c.events = append(c.events, user.NewHumanPhoneVerifiedEvent(ctx, c.aggregate))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err = c.eventstore.Push(ctx, user.NewHumanPhoneVerificationFailedEvent(ctx, c.aggregate))
|
||||||
|
logging.WithFields("id", "COMMAND-Zoo6b", "userID", c.aggregate.ID).OnError(err).Error("NewHumanPhoneVerificationFailedEvent push failed")
|
||||||
|
return caos_errs.ThrowInvalidArgument(err, "COMMAND-eis9R", "Errors.User.Code.Invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push all events to the eventstore and Reduce them into the Model.
|
||||||
|
func (c *UserPhoneEvents) Push(ctx context.Context) (*domain.Phone, error) {
|
||||||
|
pushedEvents, err := c.eventstore.Push(ctx, c.events...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(c.model, pushedEvents...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
phone := writeModelToPhone(c.model)
|
||||||
|
phone.PlainCode = c.plainCode
|
||||||
|
|
||||||
|
return phone, nil
|
||||||
|
}
|
759
internal/command/user_v2_phone_test.go
Normal file
759
internal/command/user_v2_phone_test.go
Normal file
@ -0,0 +1,759 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"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/eventstore/repository"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommands_ChangeUserPhone(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
userID string
|
||||||
|
resourceOwner string
|
||||||
|
phone 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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing phone",
|
||||||
|
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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not changed",
|
||||||
|
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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234567",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Phone.NotChanged"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
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)))
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
// successful cases are tested in TestCommands_changeUserPhoneWithGenerator
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_ChangeUserPhoneReturnCode(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
userID string
|
||||||
|
resourceOwner string
|
||||||
|
phone 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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234567",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing phone",
|
||||||
|
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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.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.ChangeUserPhoneReturnCode(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone, crypto.CreateMockEncryptionAlg(gomock.NewController(t)))
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
// successful cases are tested in TestCommands_changeUserPhoneWithGenerator
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_ChangeUserPhoneVerified(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
userID string
|
||||||
|
resourceOwner string
|
||||||
|
phone string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *domain.Phone
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing userID",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t),
|
||||||
|
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
userID: "",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234567",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234567",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing phone",
|
||||||
|
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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "phone changed",
|
||||||
|
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
|
||||||
|
}(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanPhoneChangedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"+41791234568",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanPhoneVerifiedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
userID: "user1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234568",
|
||||||
|
},
|
||||||
|
want: &domain.Phone{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
PhoneNumber: domain.PhoneNumber("+41791234568"),
|
||||||
|
IsPhoneVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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.ChangeUserPhoneVerified(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, got, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_changeUserPhoneWithGenerator(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
userID string
|
||||||
|
resourceOwner string
|
||||||
|
phone 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: "",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234567",
|
||||||
|
returnCode: false,
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234567",
|
||||||
|
returnCode: false,
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing phone",
|
||||||
|
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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "",
|
||||||
|
returnCode: false,
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not changed",
|
||||||
|
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",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234567",
|
||||||
|
returnCode: false,
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Phone.NotChanged"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "phone changed",
|
||||||
|
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
|
||||||
|
}(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanPhoneChangedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"+41791234568",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
userID: "user1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234568",
|
||||||
|
returnCode: false,
|
||||||
|
},
|
||||||
|
want: &domain.Phone{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
PhoneNumber: "+41791234568",
|
||||||
|
IsPhoneVerified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "phone changed, 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
|
||||||
|
}(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanPhoneChangedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"+41791234568",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
userID: "user1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
phone: "+41791234568",
|
||||||
|
returnCode: true,
|
||||||
|
},
|
||||||
|
want: &domain.Phone{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "user1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
PhoneNumber: "+41791234568",
|
||||||
|
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.changeUserPhoneWithGenerator(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.phone, GetMockSecretGenerator(t), tt.args.returnCode)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, got, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,8 @@ type Phone struct {
|
|||||||
|
|
||||||
PhoneNumber PhoneNumber
|
PhoneNumber PhoneNumber
|
||||||
IsPhoneVerified bool
|
IsPhoneVerified bool
|
||||||
|
// PlainCode is set by the command and can be used to return it to the caller (API)
|
||||||
|
PlainCode *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhoneCode struct {
|
type PhoneCode struct {
|
||||||
|
@ -91,6 +91,12 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
|
|||||||
ReturnCode: &user.ReturnEmailVerificationCode{},
|
ReturnCode: &user.ReturnEmailVerificationCode{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Phone: &user.SetHumanPhone{
|
||||||
|
Phone: "+41791234567",
|
||||||
|
Verification: &user.SetHumanPhone_ReturnCode{
|
||||||
|
ReturnCode: &user.ReturnPhoneVerificationCode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
logging.OnError(err).Fatal("create human user")
|
logging.OnError(err).Fatal("create human user")
|
||||||
return resp
|
return resp
|
||||||
|
@ -182,6 +182,7 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
|
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.CodeReturned {
|
if e.CodeReturned {
|
||||||
return crdb.NewNoOpStatement(e), nil
|
return crdb.NewNoOpStatement(e), nil
|
||||||
}
|
}
|
||||||
@ -535,6 +536,9 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-He83g", "reduce.wrong.event.type %s", user.HumanPhoneCodeAddedType)
|
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-He83g", "reduce.wrong.event.type %s", user.HumanPhoneCodeAddedType)
|
||||||
}
|
}
|
||||||
|
if e.CodeReturned {
|
||||||
|
return crdb.NewNoOpStatement(e), nil
|
||||||
|
}
|
||||||
ctx := HandlerContext(event.Aggregate())
|
ctx := HandlerContext(event.Aggregate())
|
||||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||||
user.UserV1PhoneCodeAddedType, user.UserV1PhoneCodeSentType,
|
user.UserV1PhoneCodeAddedType, user.UserV1PhoneCodeSentType,
|
||||||
|
@ -151,6 +151,7 @@ type HumanPhoneCodeAddedEvent struct {
|
|||||||
|
|
||||||
Code *crypto.CryptoValue `json:"code,omitempty"`
|
Code *crypto.CryptoValue `json:"code,omitempty"`
|
||||||
Expiry time.Duration `json:"expiry,omitempty"`
|
Expiry time.Duration `json:"expiry,omitempty"`
|
||||||
|
CodeReturned bool `json:"code_returned,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *HumanPhoneCodeAddedEvent) Data() interface{} {
|
func (e *HumanPhoneCodeAddedEvent) Data() interface{} {
|
||||||
@ -166,6 +167,15 @@ func NewHumanPhoneCodeAddedEvent(
|
|||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
code *crypto.CryptoValue,
|
code *crypto.CryptoValue,
|
||||||
expiry time.Duration,
|
expiry time.Duration,
|
||||||
|
) *HumanPhoneCodeAddedEvent {
|
||||||
|
return NewHumanPhoneCodeAddedEventV2(ctx, aggregate, code, expiry, false)
|
||||||
|
}
|
||||||
|
func NewHumanPhoneCodeAddedEventV2(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
code *crypto.CryptoValue,
|
||||||
|
expiry time.Duration,
|
||||||
|
codeReturned bool,
|
||||||
) *HumanPhoneCodeAddedEvent {
|
) *HumanPhoneCodeAddedEvent {
|
||||||
return &HumanPhoneCodeAddedEvent{
|
return &HumanPhoneCodeAddedEvent{
|
||||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
@ -175,6 +185,7 @@ func NewHumanPhoneCodeAddedEvent(
|
|||||||
),
|
),
|
||||||
Code: code,
|
Code: code,
|
||||||
Expiry: expiry,
|
Expiry: expiry,
|
||||||
|
CodeReturned: codeReturned,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
30
proto/zitadel/user/v2alpha/phone.proto
Normal file
30
proto/zitadel/user/v2alpha/phone.proto
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.user.v2alpha;
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
||||||
|
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
|
message SetHumanPhone {
|
||||||
|
string phone = 1 [
|
||||||
|
(validate.rules).string = {min_len: 0, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"+41791234567\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
oneof verification {
|
||||||
|
SendPhoneVerificationCode send_code = 2;
|
||||||
|
ReturnPhoneVerificationCode return_code = 3;
|
||||||
|
bool is_verified = 4 [(validate.rules).bool.const = true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendPhoneVerificationCode {}
|
||||||
|
|
||||||
|
message ReturnPhoneVerificationCode {}
|
||||||
|
|
@ -6,6 +6,7 @@ import "zitadel/object/v2alpha/object.proto";
|
|||||||
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||||
import "zitadel/user/v2alpha/auth.proto";
|
import "zitadel/user/v2alpha/auth.proto";
|
||||||
import "zitadel/user/v2alpha/email.proto";
|
import "zitadel/user/v2alpha/email.proto";
|
||||||
|
import "zitadel/user/v2alpha/phone.proto";
|
||||||
import "zitadel/user/v2alpha/idp.proto";
|
import "zitadel/user/v2alpha/idp.proto";
|
||||||
import "zitadel/user/v2alpha/password.proto";
|
import "zitadel/user/v2alpha/password.proto";
|
||||||
import "zitadel/user/v2alpha/user.proto";
|
import "zitadel/user/v2alpha/user.proto";
|
||||||
@ -158,6 +159,56 @@ service UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change the phone of a user
|
||||||
|
rpc SetPhone(SetPhoneRequest) returns (SetPhoneResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v2alpha/users/{user_id}/phone"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "Change the user phone";
|
||||||
|
description: "Change the phone number of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by sms."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the phone with the provided code
|
||||||
|
rpc VerifyPhone (VerifyPhoneRequest) returns (VerifyPhoneResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v2alpha/users/{user_id}/phone/_verify"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "Verify the phone";
|
||||||
|
description: "Verify the phone with the generated code."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
rpc RegisterPasskey (RegisterPasskeyRequest) returns (RegisterPasskeyResponse) {
|
rpc RegisterPasskey (RegisterPasskeyRequest) returns (RegisterPasskeyResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/v2alpha/users/{user_id}/passkeys"
|
post: "/v2alpha/users/{user_id}/passkeys"
|
||||||
@ -584,6 +635,7 @@ message AddHumanUserRequest{
|
|||||||
(validate.rules).message.required = true,
|
(validate.rules).message.required = true,
|
||||||
(google.api.field_behavior) = REQUIRED
|
(google.api.field_behavior) = REQUIRED
|
||||||
];
|
];
|
||||||
|
SetHumanPhone phone = 10;
|
||||||
repeated SetMetadataEntry metadata = 6;
|
repeated SetMetadataEntry metadata = 6;
|
||||||
oneof password_type {
|
oneof password_type {
|
||||||
Password password = 7;
|
Password password = 7;
|
||||||
@ -596,6 +648,7 @@ message AddHumanUserResponse {
|
|||||||
string user_id = 1;
|
string user_id = 1;
|
||||||
zitadel.object.v2alpha.Details details = 2;
|
zitadel.object.v2alpha.Details details = 2;
|
||||||
optional string email_code = 3;
|
optional string email_code = 3;
|
||||||
|
optional string phone_code = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetEmailRequest{
|
message SetEmailRequest{
|
||||||
@ -657,6 +710,65 @@ message VerifyEmailResponse{
|
|||||||
zitadel.object.v2alpha.Details details = 1;
|
zitadel.object.v2alpha.Details details = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetPhoneRequest{
|
||||||
|
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\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
string phone = 2 [
|
||||||
|
(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: "\"+41791234567\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// if no verification is specified, an sms is sent
|
||||||
|
oneof verification {
|
||||||
|
SendPhoneVerificationCode send_code = 3;
|
||||||
|
ReturnPhoneVerificationCode return_code = 4;
|
||||||
|
bool is_verified = 5 [(validate.rules).bool.const = true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetPhoneResponse{
|
||||||
|
zitadel.object.v2alpha.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},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"69629026806489455\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
string verification_code = 2 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 20},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 20;
|
||||||
|
example: "\"SKJd342k\"";
|
||||||
|
description: "\"the verification code generated during the set phone request\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyPhoneResponse{
|
||||||
|
zitadel.object.v2alpha.Details details = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message RegisterPasskeyRequest{
|
message RegisterPasskeyRequest{
|
||||||
string user_id = 1 [
|
string user_id = 1 [
|
||||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user