mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-05 14:37:45 +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
|
||||
}
|
||||
|
||||
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) {
|
||||
var phone *domain.Phone
|
||||
switch v := req.GetVerification().(type) {
|
||||
|
@ -3,6 +3,7 @@
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"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
|
||||
}
|
||||
|
||||
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,
|
||||
// operating on the Human Phone Model.
|
||||
type UserPhoneEvents struct {
|
||||
@ -191,6 +214,14 @@ func (c *UserPhoneEvents) Change(ctx context.Context, phone domain.PhoneNumber)
|
||||
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.
|
||||
func (c *UserPhoneEvents) SetVerified(ctx context.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
|
||||
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) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2beta/users/{user_id}/phone/resend"
|
||||
@ -1124,6 +1148,22 @@ message SetPhoneResponse{
|
||||
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{
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
|
Loading…
x
Reference in New Issue
Block a user