mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-31 11:03:44 +00:00
feat: add delete phone endpoint to v2 api (#8321)
# Which Problems Are Solved - Adds delete phone endpoint to v2 api # How the Problems Are Solved - Adds new endpoint with DELETE method to /v2beta/users/:userId/phone which removes currently set phone number # Additional Changes - Added integration test for new endpoint. # Additional Context - Solves https://discord.com/channels/927474939156643850/1255557862286032996
This commit is contained in:
parent
8d13247413
commit
f34897a8c8
@ -40,6 +40,23 @@ func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) RemovePhone(ctx context.Context, req *user.RemovePhoneRequest) (resp *user.RemovePhoneResponse, err error) {
|
||||||
|
details, err := s.command.RemoveUserPhone(ctx,
|
||||||
|
req.GetUserId(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user.RemovePhoneResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: details.Sequence,
|
||||||
|
ChangeDate: timestamppb.New(details.EventDate),
|
||||||
|
ResourceOwner: details.ResourceOwner,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) {
|
func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) {
|
||||||
var phone *domain.Phone
|
var phone *domain.Phone
|
||||||
switch v := req.GetVerification().(type) {
|
switch v := req.GetVerification().(type) {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
package user_test
|
package user_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -245,3 +246,99 @@ func TestServer_VerifyPhone(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_RemovePhone(t *testing.T) {
|
||||||
|
userResp := Tester.CreateHumanUser(CTX)
|
||||||
|
failResp := Tester.CreateHumanUserNoPhone(CTX)
|
||||||
|
otherUser := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
doubleRemoveUser := Tester.CreateHumanUser(CTX)
|
||||||
|
|
||||||
|
Tester.RegisterUserPasskey(CTX, otherUser)
|
||||||
|
_, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
req *user.RemovePhoneRequest
|
||||||
|
want *user.RemovePhoneResponse
|
||||||
|
wantErr bool
|
||||||
|
dep func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "remove phone",
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RemovePhoneRequest{
|
||||||
|
UserId: userResp.GetUserId(),
|
||||||
|
},
|
||||||
|
want: &user.RemovePhoneResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: 1,
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user without phone",
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RemovePhoneRequest{
|
||||||
|
UserId: failResp.GetUserId(),
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove previously deleted phone",
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RemovePhoneRequest{
|
||||||
|
UserId: doubleRemoveUser.GetUserId(),
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||||
|
return Client.RemovePhone(ctx, &user.RemovePhoneRequest{
|
||||||
|
UserId: doubleRemoveUser.GetUserId(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no user id",
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RemovePhoneRequest{},
|
||||||
|
wantErr: true,
|
||||||
|
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other user, no permission",
|
||||||
|
ctx: Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser),
|
||||||
|
req: &user.RemovePhoneRequest{
|
||||||
|
UserId: userResp.GetUserId(),
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, depErr := tt.dep(tt.ctx, tt.req.UserId)
|
||||||
|
require.NoError(t, depErr)
|
||||||
|
|
||||||
|
got, err := Client.RemovePhone(tt.ctx, tt.req)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
integration.AssertDetails(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -140,6 +140,29 @@ func (c *Commands) verifyUserPhoneWithGenerator(ctx context.Context, userID, cod
|
|||||||
return writeModelToObjectDetails(&cmd.model.WriteModel), nil
|
return writeModelToObjectDetails(&cmd.model.WriteModel), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Commands) RemoveUserPhone(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
|
||||||
|
return c.removeUserPhone(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) removeUserPhone(ctx context.Context, userID string) (*domain.ObjectDetails, 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 err = cmd.Remove(ctx); 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,
|
// UserPhoneEvents allows step-by-step additions of events,
|
||||||
// operating on the Human Phone Model.
|
// operating on the Human Phone Model.
|
||||||
type UserPhoneEvents struct {
|
type UserPhoneEvents struct {
|
||||||
@ -191,6 +214,14 @@ func (c *UserPhoneEvents) Change(ctx context.Context, phone domain.PhoneNumber)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *UserPhoneEvents) Remove(ctx context.Context) error {
|
||||||
|
if c.model.State == domain.PhoneStateRemoved || c.model.State == domain.PhoneStateUnspecified {
|
||||||
|
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-ieJ2e", "Errors.User.Phone.NotFound")
|
||||||
|
}
|
||||||
|
c.events = append(c.events, user.NewHumanPhoneRemovedEvent(ctx, c.aggregate))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetVerified sets the phone number to verified.
|
// SetVerified sets the phone number to verified.
|
||||||
func (c *UserPhoneEvents) SetVerified(ctx context.Context) {
|
func (c *UserPhoneEvents) SetVerified(ctx context.Context) {
|
||||||
c.events = append(c.events, user.NewHumanPhoneVerifiedEvent(ctx, c.aggregate))
|
c.events = append(c.events, user.NewHumanPhoneVerifiedEvent(ctx, c.aggregate))
|
||||||
|
@ -153,6 +153,30 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user.AddHumanUserResponse {
|
||||||
|
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
||||||
|
Organization: &object.Organization{
|
||||||
|
Org: &object.Organization_OrgId{
|
||||||
|
OrgId: s.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Profile: &user.SetHumanProfile{
|
||||||
|
GivenName: "Mickey",
|
||||||
|
FamilyName: "Mouse",
|
||||||
|
PreferredLanguage: gu.Ptr("nl"),
|
||||||
|
Gender: gu.Ptr(user.Gender_GENDER_MALE),
|
||||||
|
},
|
||||||
|
Email: &user.SetHumanEmail{
|
||||||
|
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
|
||||||
|
Verification: &user.SetHumanEmail_ReturnCode{
|
||||||
|
ReturnCode: &user.ReturnEmailVerificationCode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
logging.OnError(err).Fatal("create human user")
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user.AddHumanUserResponse {
|
func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user.AddHumanUserResponse {
|
||||||
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
||||||
Organization: &object.Organization{
|
Organization: &object.Organization{
|
||||||
|
@ -305,6 +305,30 @@ service UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
delete: "/v2beta/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: "Delete the user phone";
|
||||||
|
description: "Delete the phone number of a user."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
rpc ResendPhoneCode (ResendPhoneCodeRequest) returns (ResendPhoneCodeResponse) {
|
rpc ResendPhoneCode (ResendPhoneCodeRequest) returns (ResendPhoneCodeResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/v2beta/users/{user_id}/phone/resend"
|
post: "/v2beta/users/{user_id}/phone/resend"
|
||||||
@ -1124,6 +1148,22 @@ message SetPhoneResponse{
|
|||||||
optional string verification_code = 2;
|
optional string verification_code = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RemovePhoneRequest{
|
||||||
|
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\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemovePhoneResponse{
|
||||||
|
zitadel.object.v2beta.Details details = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message ResendPhoneCodeRequest{
|
message ResendPhoneCodeRequest{
|
||||||
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