Skip to content

Commit 7366529

Browse files
add user patch endpoints
1 parent 3e74fcc commit 7366529

File tree

4 files changed

+244
-4
lines changed

4 files changed

+244
-4
lines changed

internal/handlers/server.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
"github.com/nevzattalhaozcan/forgotten/pkg/logger"
1010
"github.com/prometheus/client_golang/prometheus/promhttp"
1111
"github.com/redis/go-redis/v9"
12-
"github.com/swaggo/files"
13-
"github.com/swaggo/gin-swagger"
12+
swaggerFiles "github.com/swaggo/files"
13+
ginSwagger "github.com/swaggo/gin-swagger"
1414

1515
"github.com/gin-contrib/cors"
1616
"github.com/gin-gonic/gin"
@@ -149,6 +149,10 @@ func (s *Server) setupRoutes() {
149149
protected.GET("/users", middleware.RestrictToRoles("admin", "superuser"), userHandler.GetAllUsers)
150150
protected.PUT("/users/:id", middleware.AuthorizeSelf(), userHandler.UpdateUser)
151151
protected.DELETE("/users/:id", middleware.AuthorizeSelf(), userHandler.DeleteUser)
152+
protected.PATCH("/users/:id/password", userHandler.PatchPassword)
153+
protected.PATCH("/users/:id/profile", userHandler.PatchProfile)
154+
protected.PATCH("/users/:id/account", userHandler.PatchAccount)
155+
protected.PATCH("/users/:id/avatar", userHandler.PatchAvatar)
152156

153157
protected.POST("/clubs", clubHandler.CreateClub)
154158
protected.PUT("/clubs/:id", middleware.RequireClubMembershipWithRoles(clubRepo, "club_admin"), clubHandler.UpdateClub)
@@ -159,7 +163,7 @@ func (s *Server) setupRoutes() {
159163
protected.POST("/clubs/:id/leave", middleware.RequireClubMembership(clubRepo), clubHandler.LeaveClub)
160164
protected.POST("/clubs/:id/ratings", middleware.RequireClubMembership(clubRepo), clubHandler.RateClub)
161165
protected.GET("/my-clubs", clubHandler.GetMyClubs)
162-
166+
163167
protected.PUT("/clubs/:id/members/:user_id", middleware.RequireClubMembershipWithRoles(clubRepo, "club_admin", "moderator"), clubHandler.UpdateClubMember)
164168
protected.GET("/clubs/:id/members/:user_id", clubHandler.GetClubMember)
165169

@@ -184,7 +188,7 @@ func (s *Server) setupRoutes() {
184188

185189
protected.POST("/posts/:id/vote", middleware.RequireClubMembership(clubRepo), postHandler.VoteOnPoll)
186190
protected.POST("/posts/:id/unvote", middleware.RequireClubMembership(clubRepo), postHandler.RemoveVoteFromPoll)
187-
protected.GET("/posts/:id/poll/votes", middleware.RequireClubMembership(clubRepo), postHandler.GetUserPollVotes)
191+
protected.GET("/posts/:id/poll/votes", middleware.RequireClubMembership(clubRepo), postHandler.GetUserPollVotes)
188192

189193
protected.POST("/posts/:id/like", middleware.RequireClubMembership(clubRepo), postHandler.LikePost)
190194
protected.POST("/posts/:id/unlike", middleware.RequireClubMembership(clubRepo), postHandler.UnlikePost)

internal/handlers/user.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,103 @@ func (h *UserHandler) DeleteUser(c *gin.Context) {
227227
}
228228

229229
c.JSON(http.StatusNoContent, nil)
230+
}
231+
232+
func (h *UserHandler) PatchPassword(c *gin.Context) {
233+
var req models.UpdatePasswordRequest
234+
235+
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
236+
237+
if err := c.ShouldBindJSON(&req); err != nil {
238+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
239+
return
240+
}
241+
if err := h.validator.Struct(req); err != nil {
242+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
243+
return
244+
}
245+
246+
err := h.userService.UpdatePassword(uint(id), &req)
247+
if err != nil {
248+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
249+
return
250+
}
251+
}
252+
253+
func (h *UserHandler) PatchProfile(c *gin.Context) {
254+
var req models.UpdateUserRequest
255+
256+
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
257+
258+
if err := c.ShouldBindJSON(&req); err != nil {
259+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
260+
return
261+
}
262+
if err := h.validator.Struct(req); err != nil {
263+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
264+
return
265+
}
266+
267+
user, err := h.userService.UpdateUser(uint(id), &req)
268+
if err != nil {
269+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
270+
return
271+
}
272+
273+
c.JSON(http.StatusOK, gin.H{
274+
"message": "user updated successfully",
275+
"user": user,
276+
})
277+
}
278+
279+
func (h *UserHandler) PatchAvatar(c *gin.Context) {
280+
var req models.UpdateAvatarRequest
281+
282+
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
283+
284+
if err := c.ShouldBindJSON(&req); err != nil {
285+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
286+
return
287+
}
288+
if err := h.validator.Struct(req); err != nil {
289+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
290+
return
291+
}
292+
293+
user, err := h.userService.UpdateAvatar(uint(id), &req)
294+
if err != nil {
295+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
296+
return
297+
}
298+
299+
c.JSON(http.StatusOK, gin.H{
300+
"message": "avatar updated successfully",
301+
"user": user,
302+
})
303+
}
304+
305+
func (h *UserHandler) PatchAccount(c *gin.Context) {
306+
var req models.UpdateAccountRequest
307+
308+
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
309+
310+
if err := c.ShouldBindJSON(&req); err != nil {
311+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
312+
return
313+
}
314+
if err := h.validator.Struct(req); err != nil {
315+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
316+
return
317+
}
318+
319+
user, err := h.userService.UpdateAccount(uint(id), &req)
320+
if err != nil {
321+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
322+
return
323+
}
324+
325+
c.JSON(http.StatusOK, gin.H{
326+
"message": "account updated successfully",
327+
"user": user,
328+
})
230329
}

internal/models/user.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,29 @@ type UpdateUserRequest struct {
104104
ReadingGoal *int `json:"reading_goal" validate:"omitempty,gte=0"`
105105
}
106106

107+
type UpdatePasswordRequest struct {
108+
Password string `json:"password" validate:"required,min=6"`
109+
NewPassword string `json:"new_password" validate:"required,min=6"`
110+
}
111+
112+
type UpdateProfileRequest struct {
113+
Bio *string `json:"bio,omitempty" validate:"omitempty"`
114+
Location *string `json:"location,omitempty" validate:"omitempty,max=255"`
115+
FavoriteGenres *[]string `json:"favorite_genres,omitempty"`
116+
ReadingGoal *int `json:"reading_goal,omitempty" validate:"omitempty,gte=0"`
117+
}
118+
119+
type UpdateAvatarRequest struct {
120+
AvatarURL string `json:"avatar_url" validate:"required,url"`
121+
}
122+
123+
type UpdateAccountRequest struct {
124+
FirstName *string `json:"first_name,omitempty" validate:"omitempty,min=2,max=50"`
125+
LastName *string `json:"last_name,omitempty" validate:"omitempty,min=2,max=50"`
126+
Email *string `json:"email,omitempty" validate:"omitempty,email"`
127+
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"`
128+
}
129+
107130
func (u *User) ToResponse() UserResponse {
108131
return UserResponse{
109132
ID: u.ID,

internal/services/user.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,118 @@ func (s *UserService) DeleteUser(id uint) error {
226226
metrics.IncrementUserCount(-1)
227227

228228
return nil
229+
}
230+
231+
func (s *UserService) UpdatePassword(id uint, req *models.UpdatePasswordRequest) error {
232+
user, err := s.userRepo.GetByID(id)
233+
if err != nil {
234+
if errors.Is(err, gorm.ErrRecordNotFound) {
235+
return errors.New("user not found")
236+
}
237+
return err
238+
}
239+
240+
if !utils.CheckPasswordHash(req.Password, user.PasswordHash) {
241+
return errors.New("current password is incorrect")
242+
}
243+
244+
hashedPassword, err := utils.HashPassword(req.NewPassword)
245+
if err != nil {
246+
return errors.New("failed to hash new password")
247+
}
248+
249+
user.PasswordHash = hashedPassword
250+
return s.userRepo.Update(user)
251+
}
252+
253+
func (s *UserService) UpdateProfile(id uint, req *models.UpdateProfileRequest) (*models.UserResponse, error) {
254+
user, err := s.userRepo.GetByID(id)
255+
if err != nil {
256+
if errors.Is(err, gorm.ErrRecordNotFound) {
257+
return nil, errors.New("user not found")
258+
}
259+
return nil, err
260+
}
261+
262+
if req.Bio != nil {
263+
user.Bio = req.Bio
264+
}
265+
266+
if req.Location != nil {
267+
user.Location = req.Location
268+
}
269+
270+
if req.FavoriteGenres != nil {
271+
user.FavoriteGenres = *req.FavoriteGenres
272+
}
273+
274+
if req.ReadingGoal != nil {
275+
user.ReadingGoal = *req.ReadingGoal
276+
}
277+
278+
if err := s.userRepo.Update(user); err != nil {
279+
return nil, errors.New("failed to update profile")
280+
}
281+
282+
response := user.ToResponse()
283+
return &response, nil
284+
}
285+
286+
func (s *UserService) UpdateAvatar(id uint, req *models.UpdateAvatarRequest) (*models.UserResponse, error) {
287+
user, err := s.userRepo.GetByID(id)
288+
if err != nil {
289+
if errors.Is(err, gorm.ErrRecordNotFound) {
290+
return nil, errors.New("user not found")
291+
}
292+
return nil, err
293+
}
294+
295+
user.AvatarURL = &req.AvatarURL
296+
if err := s.userRepo.Update(user); err != nil {
297+
return nil, errors.New("failed to update avatar")
298+
}
299+
300+
response := user.ToResponse()
301+
return &response, nil
302+
}
303+
304+
func (s *UserService) UpdateAccount(id uint, req *models.UpdateAccountRequest) (*models.UserResponse, error) {
305+
user, err := s.userRepo.GetByID(id)
306+
if err != nil {
307+
if errors.Is(err, gorm.ErrRecordNotFound) {
308+
return nil, errors.New("user not found")
309+
}
310+
return nil, err
311+
}
312+
313+
if req.FirstName != nil {
314+
user.FirstName = *req.FirstName
315+
}
316+
317+
if req.LastName != nil {
318+
user.LastName = *req.LastName
319+
}
320+
321+
if req.Email != nil && *req.Email != user.Email {
322+
existingUser, err := s.userRepo.GetByEmail(*req.Email)
323+
if err == nil && existingUser.ID != user.ID {
324+
return nil, errors.New("email already exists")
325+
}
326+
user.Email = *req.Email
327+
}
328+
329+
if req.Username != nil && *req.Username != user.Username {
330+
existingUser, err := s.userRepo.GetByUsername(*req.Username)
331+
if err == nil && existingUser.ID != user.ID {
332+
return nil, errors.New("username already exists")
333+
}
334+
user.Username = *req.Username
335+
}
336+
337+
if err := s.userRepo.Update(user); err != nil {
338+
return nil, errors.New("failed to update account")
339+
}
340+
341+
response := user.ToResponse()
342+
return &response, nil
229343
}

0 commit comments

Comments
 (0)