diff --git a/internal/user/form.go b/internal/user/form.go index 395897e..80c23e3 100644 --- a/internal/user/form.go +++ b/internal/user/form.go @@ -1,6 +1,10 @@ package user -import "tm/pkg/response" +import ( + "encoding/json" + + "tm/pkg/response" +) // User Registration DTOs @@ -27,6 +31,46 @@ type UpdateUserForm struct { Position *string `json:"position" valid:"optional,length(2|100)" example:"Lead"` // Updated position Phone *string `json:"phone" valid:"optional,length(10|20)" example:"+18289784438"` // Updated phone number ProfileImage *string `json:"profile_image" valid:"optional,url" example:""` // Updated profile image URL + profileImageSet bool +} + +func unmarshalProfileImageField(data []byte, profileImage **string, profileImageSet *bool) ([]byte, error) { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + profileImageRaw, ok := raw["profile_image"] + if ok { + *profileImageSet = true + if string(profileImageRaw) == "null" { + *profileImage = nil + } else { + var image string + if err := json.Unmarshal(profileImageRaw, &image); err != nil { + return nil, err + } + *profileImage = &image + } + delete(raw, "profile_image") + } + + return json.Marshal(raw) +} + +// UnmarshalJSON distinguishes omitted profile_image from explicit null to allow clearing the image. +func (f *UpdateUserForm) UnmarshalJSON(data []byte) error { + rest, err := unmarshalProfileImageField(data, &f.ProfileImage, &f.profileImageSet) + if err != nil { + return err + } + + type alias UpdateUserForm + return json.Unmarshal(rest, (*alias)(f)) +} + +func (f *UpdateUserForm) isProfileImageSet() bool { + return f.profileImageSet } // User Authentication DTOs @@ -51,11 +95,27 @@ type ChangePasswordForm struct { // UpdateProfileForm represents the data for updating user profile information type UpdateProfileForm struct { - FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"Admin User"` // Updated full name - Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Information Technology"` // Updated department - Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Lead"` // Updated position - Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+18289784438"` // Updated phone number - ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:""` // Updated profile image URL + FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"Admin User"` // Updated full name + Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Information Technology"` // Updated department + Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Lead"` // Updated position + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+18289784438"` // Updated phone number + ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:""` // Updated profile image URL + profileImageSet bool +} + +// UnmarshalJSON distinguishes omitted profile_image from explicit null to allow clearing the image. +func (f *UpdateProfileForm) UnmarshalJSON(data []byte) error { + rest, err := unmarshalProfileImageField(data, &f.ProfileImage, &f.profileImageSet) + if err != nil { + return err + } + + type alias UpdateProfileForm + return json.Unmarshal(rest, (*alias)(f)) +} + +func (f *UpdateProfileForm) isProfileImageSet() bool { + return f.profileImageSet } // ResetPasswordForm represents the email address for password reset diff --git a/internal/user/form_test.go b/internal/user/form_test.go new file mode 100644 index 0000000..ecc3a84 --- /dev/null +++ b/internal/user/form_test.go @@ -0,0 +1,47 @@ +package user + +import ( + "encoding/json" + "testing" +) + +func TestUpdateUserForm_UnmarshalJSON_ProfileImage(t *testing.T) { + t.Run("omitted profile_image is not treated as an update", func(t *testing.T) { + var form UpdateUserForm + err := json.Unmarshal([]byte(`{"full_name":"Jane Doe"}`), &form) + if err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + if form.isProfileImageSet() { + t.Fatal("expected profile image to be unset when omitted") + } + }) + + t.Run("null profile_image clears the image", func(t *testing.T) { + var form UpdateUserForm + err := json.Unmarshal([]byte(`{"profile_image":null}`), &form) + if err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + if !form.isProfileImageSet() { + t.Fatal("expected profile image to be marked as set") + } + if form.ProfileImage != nil { + t.Fatalf("expected nil profile image, got %#v", form.ProfileImage) + } + }) + + t.Run("profile image url is preserved", func(t *testing.T) { + var form UpdateUserForm + err := json.Unmarshal([]byte(`{"profile_image":"https://example.com/avatar.png"}`), &form) + if err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + if !form.isProfileImageSet() { + t.Fatal("expected profile image to be marked as set") + } + if form.ProfileImage == nil || *form.ProfileImage != "https://example.com/avatar.png" { + t.Fatalf("unexpected profile image: %#v", form.ProfileImage) + } + }) +} diff --git a/internal/user/service.go b/internal/user/service.go index b852df9..e3d3b92 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -204,7 +204,7 @@ func (s *userService) Update(ctx context.Context, id string, form *UpdateUserFor user.Phone = form.Phone } - if form.ProfileImage != nil { + if form.isProfileImageSet() { user.ProfileImage = form.ProfileImage } @@ -601,7 +601,7 @@ func (s *userService) UpdateProfile(ctx context.Context, userID string, form *Up } user.Phone = form.Phone } - if form.ProfileImage != nil { + if form.isProfileImageSet() { user.ProfileImage = form.ProfileImage }