From 59525351ae4052739bcf7b0c690c137c17c1f6be Mon Sep 17 00:00:00 2001 From: maulerrr <211239@astanait.edu.kz> Date: Fri, 23 Aug 2024 23:48:47 +0500 Subject: [PATCH 1/8] feature: basic user crud implemented profile picture, achievements, privacy, friends etc will be added soon --- backend/internal/modules/user/handler.go | 68 +++++++++++++++++++ backend/internal/modules/user/repo.go | 45 ++++++++++++ backend/internal/modules/user/service.go | 60 ++++++++++++++++ .../internal/modules/user/types/user-model.go | 17 +++++ 4 files changed, 190 insertions(+) create mode 100644 backend/internal/modules/user/handler.go create mode 100644 backend/internal/modules/user/repo.go create mode 100644 backend/internal/modules/user/service.go create mode 100644 backend/internal/modules/user/types/user-model.go diff --git a/backend/internal/modules/user/handler.go b/backend/internal/modules/user/handler.go new file mode 100644 index 0000000..a606fe4 --- /dev/null +++ b/backend/internal/modules/user/handler.go @@ -0,0 +1,68 @@ +package user + +import ( + "net/http" + "strconv" + "takumi/internal/modules/user/types" + "takumi/pkg/utils" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + Service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{ + Service: service, + } +} + +func (h *Handler) GetUserByIDHandler(c *gin.Context) { + userID, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: Invalid user ID", http.StatusBadRequest) + return + } + + user, err := h.Service.GetUserByID(c, userID) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusNotFound) + return + } + + utils.SendSuccessJSON(c, user) +} + +func (h *Handler) DeleteUserByIDHandler(c *gin.Context) { + userID, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: Invalid user ID", http.StatusBadRequest) + return + } + + err = h.Service.DeleteUserByID(c, userID) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusNotFound) + return + } + + utils.SendMessageWithStatus(c, "User deleted successfully", http.StatusOK) +} + +func (h *Handler) UpdateUserParamsHandler(c *gin.Context) { + update := &types.User{} + if err := c.ShouldBindJSON(update); err != nil { + utils.SendMessageWithStatus(c, "ERROR: Invalid request body", http.StatusBadRequest) + return + } + + updatedUser, err := h.Service.UpdateUserParams(c, *update) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusInternalServerError) + return + } + + utils.SendSuccessJSON(c, updatedUser) +} diff --git a/backend/internal/modules/user/repo.go b/backend/internal/modules/user/repo.go new file mode 100644 index 0000000..e046d9f --- /dev/null +++ b/backend/internal/modules/user/repo.go @@ -0,0 +1,45 @@ +package user + +import ( + "errors" + "takumi/internal/database" + "takumi/internal/modules/user/types" + + "gorm.io/gorm" +) + +func getUserByID(id int, handler database.DBHandler) (*types.User, error) { + user := types.User{} + query := types.User{ID: id} + + if err := handler.DB.First(&user, &query).Error; err == gorm.ErrRecordNotFound { + return nil, errors.New("user is not found") + } + + return &user, nil +} + +func DeleteUserByID(id int, handler database.DBHandler) error { + user := types.User{} + query := types.User{ID: id} + + if err := handler.DB.First(&user, &query).Error; err != nil { + return err + } + + return handler.DB.Delete(&user).Error +} + +func UpdateUserParams(update types.User, handler database.DBHandler) (*types.User, error) { + user := types.User{} + + if err := handler.DB.First(&user, update.ID).Error; err != nil { + return nil, err + } + + if err := handler.DB.Model(&user).Updates(update).Error; err != nil { + return nil, err + } + + return &user, nil +} diff --git a/backend/internal/modules/user/service.go b/backend/internal/modules/user/service.go new file mode 100644 index 0000000..e967533 --- /dev/null +++ b/backend/internal/modules/user/service.go @@ -0,0 +1,60 @@ +package user + +import ( + "errors" + "net/http" + "takumi/internal/database" + "takumi/internal/modules/user/types" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type Service struct { + Handler database.DBHandler +} + +func InitUserService(handler database.DBHandler) (*Service, error) { + service := &Service{ + Handler: handler, + } + return service, nil +} + +func (s *Service) GetUserByID(c *gin.Context, userID int) (*types.User, error) { + user, err := getUserByID(userID, s.Handler) + if err != nil { + if err == gorm.ErrRecordNotFound { + c.AbortWithError(http.StatusNotFound, errors.New("user not found")) + return nil, errors.New("user not found") + } + c.AbortWithError(http.StatusInternalServerError, err) + return nil, errors.New("failed to retrieve user") + } + return user, nil +} + +func (s *Service) DeleteUserByID(c *gin.Context, userID int) error { + if err := DeleteUserByID(userID, s.Handler); err != nil { + if err == gorm.ErrRecordNotFound { + c.AbortWithError(http.StatusNotFound, errors.New("user not found")) + return errors.New("user not found") + } + c.AbortWithError(http.StatusInternalServerError, err) + return errors.New("failed to delete user") + } + return nil +} + +func (s *Service) UpdateUserParams(c *gin.Context, update types.User) (*types.User, error) { + user, err := UpdateUserParams(update, s.Handler) + if err != nil { + if err == gorm.ErrRecordNotFound { + c.AbortWithError(http.StatusNotFound, errors.New("user not found")) + return nil, errors.New("user not found") + } + c.AbortWithError(http.StatusInternalServerError, err) + return nil, errors.New("failed to update user") + } + return user, nil +} diff --git a/backend/internal/modules/user/types/user-model.go b/backend/internal/modules/user/types/user-model.go new file mode 100644 index 0000000..e3d3ecb --- /dev/null +++ b/backend/internal/modules/user/types/user-model.go @@ -0,0 +1,17 @@ +package types + +import "time" + +type User struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + Username string `gorm:"unique;not null" json:"username"` + FirstName string `gorm:"not null" json:"firstName"` + LastName string `gorm:"not null" json:"lastName"` + Email string `gorm:"unique;not null" json:"email"` + Gender string `json:"gender"` + BirthDate time.Time `json:"birthDate"` + Password string `gorm:"not null" json:"password"` + Role string `gorm:"default:USER" json:"role"` + CreatedAt time.Time `json:"createdAt"` + Coins int `gorm:"default:0" json:"coins"` +} From fe3d415c9906959733cc59aba0a9dfd1849f6020 Mon Sep 17 00:00:00 2001 From: maulerrr <211239@astanait.edu.kz> Date: Sun, 25 Aug 2024 21:13:40 +0500 Subject: [PATCH 2/8] feature: profile picture logic implemented maybe refactored --- backend/internal/modules/user/handler.go | 48 +++++++++++++++++++ backend/internal/modules/user/repo.go | 44 +++++++++++++++++ backend/internal/modules/user/service.go | 34 +++++++++++++ .../modules/user/types/profile-pic-model.go | 13 +++++ 4 files changed, 139 insertions(+) create mode 100644 backend/internal/modules/user/types/profile-pic-model.go diff --git a/backend/internal/modules/user/handler.go b/backend/internal/modules/user/handler.go index a606fe4..3eedffd 100644 --- a/backend/internal/modules/user/handler.go +++ b/backend/internal/modules/user/handler.go @@ -1,6 +1,7 @@ package user import ( + "io" "net/http" "strconv" "takumi/internal/modules/user/types" @@ -66,3 +67,50 @@ func (h *Handler) UpdateUserParamsHandler(c *gin.Context) { utils.SendSuccessJSON(c, updatedUser) } + +func (h *Handler) UploadProfilePictureHandler(c *gin.Context) { + userID, _ := strconv.Atoi(c.Param("userID")) + file, _, err := c.Request.FormFile("profile_picture") + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: No file uploaded", http.StatusBadRequest) + return + } + defer file.Close() + + imageData, err := io.ReadAll(file) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: Could not read file", http.StatusInternalServerError) + return + } + + picture, err := h.Service.UploadProfilePicture(c, userID, imageData) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: Could not upload profile picture", http.StatusInternalServerError) + return + } + + utils.SendSuccessJSON(c, picture) +} + +func (h *Handler) GetProfilePictureHandler(c *gin.Context) { + userID, _ := strconv.Atoi(c.Param("userID")) + picture, err := h.Service.GetProfilePicture(c, userID) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusNotFound) + return + } + + c.Header("Content-Type", "image/jpeg") + c.Writer.Write(picture.ImageData) +} + +func (h *Handler) DeleteProfilePictureHandler(c *gin.Context) { + userID, _ := strconv.Atoi(c.Param("userID")) + err := h.Service.DeleteProfilePicture(c, userID) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusInternalServerError) + return + } + + utils.SendMessageWithStatus(c, "Profile picture deleted successfully", http.StatusOK) +} diff --git a/backend/internal/modules/user/repo.go b/backend/internal/modules/user/repo.go index e046d9f..9e3d182 100644 --- a/backend/internal/modules/user/repo.go +++ b/backend/internal/modules/user/repo.go @@ -43,3 +43,47 @@ func UpdateUserParams(update types.User, handler database.DBHandler) (*types.Use return &user, nil } + +func UpsertProfilePicture(picture *types.ProfilePicture, handler database.DBHandler) (*types.ProfilePicture, error) { + existing := types.ProfilePicture{} + err := handler.DB.Where("user_id = ?", picture.UserID).First(&existing).Error + + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + + if existing.ID != 0 { + existing.ImageData = picture.ImageData + err = handler.DB.Save(&existing).Error + if err != nil { + return nil, err + } + return &existing, nil + } else { + err = handler.DB.Create(picture).Error + if err != nil { + return nil, err + } + return picture, nil + } +} + +func GetProfilePictureByUserID(userID int, handler database.DBHandler) (*types.ProfilePicture, error) { + var picture types.ProfilePicture + err := handler.DB.Where("user_id = ?", userID).First(&picture).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, errors.New("profile picture not found") + } + return nil, err + } + return &picture, nil +} + +func DeleteProfilePicture(userID int, handler database.DBHandler) error { + err := handler.DB.Where("user_id = ?", userID).Delete(&types.ProfilePicture{}).Error + if err != nil { + return err + } + return nil +} diff --git a/backend/internal/modules/user/service.go b/backend/internal/modules/user/service.go index e967533..49d5703 100644 --- a/backend/internal/modules/user/service.go +++ b/backend/internal/modules/user/service.go @@ -58,3 +58,37 @@ func (s *Service) UpdateUserParams(c *gin.Context, update types.User) (*types.Us } return user, nil } + +func (s *Service) UploadProfilePicture(c *gin.Context, userID int, imageData []byte) (*types.ProfilePicture, error) { + picture := types.ProfilePicture{ + UserID: userID, + ImageData: imageData, + } + + savedPicture, err := UpsertProfilePicture(&picture, s.Handler) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return nil, errors.New("could not upload profile picture") + } + + return savedPicture, nil +} + +func (s *Service) GetProfilePicture(c *gin.Context, userID int) (*types.ProfilePicture, error) { + picture, err := GetProfilePictureByUserID(userID, s.Handler) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return nil, errors.New("profile picture not found") + } + + return picture, nil +} + +func (s *Service) DeleteProfilePicture(c *gin.Context, userID int) error { + err := DeleteProfilePicture(userID, s.Handler) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return errors.New("could not delete profile picture") + } + return nil +} diff --git a/backend/internal/modules/user/types/profile-pic-model.go b/backend/internal/modules/user/types/profile-pic-model.go new file mode 100644 index 0000000..e67d1c6 --- /dev/null +++ b/backend/internal/modules/user/types/profile-pic-model.go @@ -0,0 +1,13 @@ +package types + +import ( + "time" +) + +type ProfilePicture struct { + ID int `gorm:"primaryKey" json:"id"` + UserID int `gorm:"not null" json:"user_id"` + ImageData []byte `gorm:"type:bytea;not null" json:"image_data"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} From 900504cc8469166fd41e349275cb6741e57d6fd7 Mon Sep 17 00:00:00 2001 From: maulerrr <211239@astanait.edu.kz> Date: Mon, 26 Aug 2024 02:52:46 +0500 Subject: [PATCH 3/8] feature: profile picture logic updated maybe store user model in users module, not in auth. also custom logger implemented, will be in use soon --- backend/cmd/main.go | 1 + backend/internal/modules/user/handler.go | 64 +++++++++++------ backend/internal/modules/user/repo.go | 71 +++++++++++-------- backend/internal/modules/user/service.go | 32 +++------ .../modules/user/types/profile-pic-dto.go | 5 ++ .../modules/user/types/profile-pic-model.go | 13 ---- .../internal/modules/user/types/user-model.go | 23 +++--- backend/pkg/utils/file.go | 44 ++++++++++++ backend/pkg/utils/logger.go | 27 +++++++ 9 files changed, 185 insertions(+), 95 deletions(-) create mode 100644 backend/internal/modules/user/types/profile-pic-dto.go delete mode 100644 backend/internal/modules/user/types/profile-pic-model.go create mode 100644 backend/pkg/utils/file.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 977b5ea..4780adf 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -19,6 +19,7 @@ func main() { dbHandler := database.InitDB(cfg.DBSource) app := setupGin(cfg) + app.Static("/uploads", "./uploads") router := routes.NewTakumiRouter(app, "/api", "/v1") authService, err := authorization.InitAuthService(dbHandler) diff --git a/backend/internal/modules/user/handler.go b/backend/internal/modules/user/handler.go index 3eedffd..31ab9cf 100644 --- a/backend/internal/modules/user/handler.go +++ b/backend/internal/modules/user/handler.go @@ -1,8 +1,10 @@ package user import ( - "io" + "fmt" + "log" "net/http" + "path/filepath" "strconv" "takumi/internal/modules/user/types" "takumi/pkg/utils" @@ -68,49 +70,69 @@ func (h *Handler) UpdateUserParamsHandler(c *gin.Context) { utils.SendSuccessJSON(c, updatedUser) } -func (h *Handler) UploadProfilePictureHandler(c *gin.Context) { - userID, _ := strconv.Atoi(c.Param("userID")) - file, _, err := c.Request.FormFile("profile_picture") +func (h *Handler) UpdateProfilePictureHandler(c *gin.Context) { + userIDParam := c.Param("id") + userID, err := strconv.Atoi(userIDParam) if err != nil { - utils.SendMessageWithStatus(c, "ERROR: No file uploaded", http.StatusBadRequest) + utils.SendMessageWithStatus(c, "ERROR: Invalid user ID", http.StatusBadRequest) return } - defer file.Close() - imageData, err := io.ReadAll(file) + file, _, err := c.Request.FormFile("profilePicture") if err != nil { - utils.SendMessageWithStatus(c, "ERROR: Could not read file", http.StatusInternalServerError) + utils.SendMessageWithStatus(c, "ERROR: File upload failed", http.StatusBadRequest) + return + } + defer file.Close() + + fileName := fmt.Sprintf("user_%d_profile_pic.jpg", userID) + if err := utils.SaveFile(file, "profile-pictures", fileName); err != nil { + log.Printf("Failed to save profile picture for user %d: %v", userID, err) + utils.SendMessageWithStatus(c, "ERROR: Could not save profile picture", http.StatusInternalServerError) return } - picture, err := h.Service.UploadProfilePicture(c, userID, imageData) + profilePictureURL := filepath.Join("profile-pictures", fileName) + updatedUser, err := h.Service.UpdateProfilePicture(c, userID, profilePictureURL) if err != nil { - utils.SendMessageWithStatus(c, "ERROR: Could not upload profile picture", http.StatusInternalServerError) + log.Printf("Error updating profile picture for user %d: %v", userID, err) + utils.SendMessageWithStatus(c, err.Error(), http.StatusInternalServerError) return } - utils.SendSuccessJSON(c, picture) + utils.SendSuccessJSON(c, updatedUser) } -func (h *Handler) GetProfilePictureHandler(c *gin.Context) { - userID, _ := strconv.Atoi(c.Param("userID")) - picture, err := h.Service.GetProfilePicture(c, userID) +func (h *Handler) GetProfilePictureByUserID(c *gin.Context) { + userIDParam := c.Param("id") + userID, err := strconv.Atoi(userIDParam) if err != nil { - utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusNotFound) + utils.SendMessageWithStatus(c, "ERROR: Invalid user ID", http.StatusBadRequest) + return + } + + profilePictureURL, err := h.Service.GetProfilePictureByUserID(userID) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusInternalServerError) return } - c.Header("Content-Type", "image/jpeg") - c.Writer.Write(picture.ImageData) + utils.SendSuccessJSON(c, gin.H{"profilePictureURL": profilePictureURL}) } func (h *Handler) DeleteProfilePictureHandler(c *gin.Context) { - userID, _ := strconv.Atoi(c.Param("userID")) - err := h.Service.DeleteProfilePicture(c, userID) + userIDParam := c.Param("id") + userID, err := strconv.Atoi(userIDParam) if err != nil { - utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusInternalServerError) + utils.SendMessageWithStatus(c, "ERROR: Invalid user ID", http.StatusBadRequest) + return + } + + deletedUser, err := h.Service.DeleteProfilePicture(c, userID) + if err != nil { + utils.SendMessageWithStatus(c, err.Error(), http.StatusInternalServerError) return } - utils.SendMessageWithStatus(c, "Profile picture deleted successfully", http.StatusOK) + utils.SendSuccessJSON(c, deletedUser) } diff --git a/backend/internal/modules/user/repo.go b/backend/internal/modules/user/repo.go index 9e3d182..30e5cd4 100644 --- a/backend/internal/modules/user/repo.go +++ b/backend/internal/modules/user/repo.go @@ -2,8 +2,10 @@ package user import ( "errors" + "log" "takumi/internal/database" "takumi/internal/modules/user/types" + "takumi/pkg/utils" "gorm.io/gorm" ) @@ -44,46 +46,57 @@ func UpdateUserParams(update types.User, handler database.DBHandler) (*types.Use return &user, nil } -func UpsertProfilePicture(picture *types.ProfilePicture, handler database.DBHandler) (*types.ProfilePicture, error) { - existing := types.ProfilePicture{} - err := handler.DB.Where("user_id = ?", picture.UserID).First(&existing).Error +func UpdateUserProfilePicture(userID int, profilePictureURL string, handler database.DBHandler) (*types.User, error) { + user := types.User{} + tx := handler.DB.Begin() - if err != nil && err != gorm.ErrRecordNotFound { + if err := tx.First(&user, userID).Error; err != nil { + tx.Rollback() return nil, err } - if existing.ID != 0 { - existing.ImageData = picture.ImageData - err = handler.DB.Save(&existing).Error - if err != nil { - return nil, err - } - return &existing, nil - } else { - err = handler.DB.Create(picture).Error - if err != nil { + if user.ProfilePicture != "" && user.ProfilePicture != "default.jpg" { + if err := utils.DeleteFile("profile-pictures", user.ProfilePicture); err != nil { + log.Printf("Failed to delete: %s, error: %v", user.ProfilePicture, err) + tx.Rollback() return nil, err } - return picture, nil } -} -func GetProfilePictureByUserID(userID int, handler database.DBHandler) (*types.ProfilePicture, error) { - var picture types.ProfilePicture - err := handler.DB.Where("user_id = ?", userID).First(&picture).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, errors.New("profile picture not found") - } + user.ProfilePicture = profilePictureURL + if err := tx.Save(&user).Error; err != nil { + tx.Rollback() return nil, err } - return &picture, nil + + tx.Commit() + return &user, nil } -func DeleteProfilePicture(userID int, handler database.DBHandler) error { - err := handler.DB.Where("user_id = ?", userID).Delete(&types.ProfilePicture{}).Error - if err != nil { - return err +func GetProfilePictureByUserID(userID int, handler database.DBHandler) (string, error) { + user := types.User{} + + if err := handler.DB.Select("profile_picture").First(&user, userID).Error; err != nil { + return "", err + } + + if user.ProfilePicture == "" { + return "", errors.New("no profile picture found for this user") } - return nil + + return user.ProfilePicture, nil +} + +func DeleteUserProfilePicture(userID int, handler database.DBHandler) (*types.User, error) { + user := types.User{} + if err := handler.DB.First(&user, userID).Error; err != nil { + return nil, err + } + + user.ProfilePicture = "" + if err := handler.DB.Save(&user).Error; err != nil { + return nil, err + } + + return &user, nil } diff --git a/backend/internal/modules/user/service.go b/backend/internal/modules/user/service.go index 49d5703..b189b48 100644 --- a/backend/internal/modules/user/service.go +++ b/backend/internal/modules/user/service.go @@ -59,36 +59,26 @@ func (s *Service) UpdateUserParams(c *gin.Context, update types.User) (*types.Us return user, nil } -func (s *Service) UploadProfilePicture(c *gin.Context, userID int, imageData []byte) (*types.ProfilePicture, error) { - picture := types.ProfilePicture{ - UserID: userID, - ImageData: imageData, - } - - savedPicture, err := UpsertProfilePicture(&picture, s.Handler) +func (s *Service) UpdateProfilePicture(c *gin.Context, userID int, profilePictureURL string) (*types.User, error) { + user, err := UpdateUserProfilePicture(userID, profilePictureURL, s.Handler) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) - return nil, errors.New("could not upload profile picture") + return nil, errors.New("could not update profile picture") } - return savedPicture, nil + return user, nil } -func (s *Service) GetProfilePicture(c *gin.Context, userID int) (*types.ProfilePicture, error) { - picture, err := GetProfilePictureByUserID(userID, s.Handler) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return nil, errors.New("profile picture not found") - } - - return picture, nil +func (s *Service) GetProfilePictureByUserID(userID int) (string, error) { + return GetProfilePictureByUserID(userID, s.Handler) } -func (s *Service) DeleteProfilePicture(c *gin.Context, userID int) error { - err := DeleteProfilePicture(userID, s.Handler) +func (s *Service) DeleteProfilePicture(c *gin.Context, userID int) (*types.User, error) { + user, err := DeleteUserProfilePicture(userID, s.Handler) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) - return errors.New("could not delete profile picture") + return nil, errors.New("could not delete profile picture") } - return nil + + return user, nil } diff --git a/backend/internal/modules/user/types/profile-pic-dto.go b/backend/internal/modules/user/types/profile-pic-dto.go new file mode 100644 index 0000000..382dc7f --- /dev/null +++ b/backend/internal/modules/user/types/profile-pic-dto.go @@ -0,0 +1,5 @@ +package types + +type ProfilePicReq struct { + ProfilePictureURL string `json:"profilePicURL" binding:"required"` +} diff --git a/backend/internal/modules/user/types/profile-pic-model.go b/backend/internal/modules/user/types/profile-pic-model.go deleted file mode 100644 index e67d1c6..0000000 --- a/backend/internal/modules/user/types/profile-pic-model.go +++ /dev/null @@ -1,13 +0,0 @@ -package types - -import ( - "time" -) - -type ProfilePicture struct { - ID int `gorm:"primaryKey" json:"id"` - UserID int `gorm:"not null" json:"user_id"` - ImageData []byte `gorm:"type:bytea;not null" json:"image_data"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} diff --git a/backend/internal/modules/user/types/user-model.go b/backend/internal/modules/user/types/user-model.go index e3d3ecb..65766fb 100644 --- a/backend/internal/modules/user/types/user-model.go +++ b/backend/internal/modules/user/types/user-model.go @@ -3,15 +3,16 @@ package types import "time" type User struct { - ID int `gorm:"primaryKey;autoIncrement" json:"id"` - Username string `gorm:"unique;not null" json:"username"` - FirstName string `gorm:"not null" json:"firstName"` - LastName string `gorm:"not null" json:"lastName"` - Email string `gorm:"unique;not null" json:"email"` - Gender string `json:"gender"` - BirthDate time.Time `json:"birthDate"` - Password string `gorm:"not null" json:"password"` - Role string `gorm:"default:USER" json:"role"` - CreatedAt time.Time `json:"createdAt"` - Coins int `gorm:"default:0" json:"coins"` + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + Username string `gorm:"unique;not null" json:"username"` + FirstName string `gorm:"not null" json:"firstName"` + LastName string `gorm:"not null" json:"lastName"` + Email string `gorm:"unique;not null" json:"email"` + Gender string `json:"gender"` + BirthDate time.Time `json:"birthDate"` + Password string `gorm:"not null" json:"password"` + Role string `gorm:"default:USER" json:"role"` + CreatedAt time.Time `json:"createdAt"` + Coins int `gorm:"default:0" json:"coins"` + ProfilePicture string `gorm:"default:''" json:"profilePicture"` } diff --git a/backend/pkg/utils/file.go b/backend/pkg/utils/file.go new file mode 100644 index 0000000..a820191 --- /dev/null +++ b/backend/pkg/utils/file.go @@ -0,0 +1,44 @@ +package utils + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +func DeleteFile(branch, filePath string) error { + fullPath := filepath.Join("uploads/", branch, filePath) + + if _, err := os.Stat(fullPath); err == nil { + if err := os.Remove(fullPath); err != nil { + return fmt.Errorf("failed to delete file %s: %w", fullPath, err) + } + } else if os.IsNotExist(err) { + return nil + } else { + return fmt.Errorf("failed to check file %s: %w", fullPath, err) + } + + return nil +} + +func SaveFile(file io.Reader, branch, filePath string) error { + fullPath := filepath.Join("uploads/", branch) + if err := os.MkdirAll(fullPath, os.ModePerm); err != nil { + return fmt.Errorf("failed to create directory %s: %w", fullPath, err) + } + + fullFilePath := filepath.Join(fullPath, filePath) + outFile, err := os.Create(fullFilePath) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", fullFilePath, err) + } + defer outFile.Close() + + if _, err := io.Copy(outFile, file); err != nil { + return fmt.Errorf("failed to copy file content to %s: %w", fullFilePath, err) + } + + return nil +} diff --git a/backend/pkg/utils/logger.go b/backend/pkg/utils/logger.go index d4b585b..10eecda 100644 --- a/backend/pkg/utils/logger.go +++ b/backend/pkg/utils/logger.go @@ -1 +1,28 @@ package utils + +import ( + "fmt" + "log" + "os" + "time" +) + +type CustomLogger struct { + logger *log.Logger +} + +func NewLogger() *CustomLogger { + return &CustomLogger{ + logger: log.New(os.Stdout, "", log.LstdFlags), + } +} + +func (l *CustomLogger) LogError(message string, err error, details map[string]interface{}) { + logMsg := fmt.Sprintf("%s | ERROR: %s | DETAILS: %v | %s", time.Now().Format(time.RFC3339), message, details, err) + l.logger.Println(logMsg) +} + +func (l *CustomLogger) LogInfo(message string, details map[string]interface{}) { + logMsg := fmt.Sprintf("%s | INFO: %s | DETAILS: %v", time.Now().Format(time.RFC3339), message, details) + l.logger.Println(logMsg) +} From ca2ef9b722510cb1032c42d2b680ba158e3ad430 Mon Sep 17 00:00:00 2001 From: maulerrr <211239@astanait.edu.kz> Date: Mon, 26 Aug 2024 03:32:32 +0500 Subject: [PATCH 4/8] feature: user/auth module cleanup --- backend/.gitignore | 3 ++- backend/cmd/main.go | 9 ++++++++ backend/internal/database/postgres.go | 2 +- .../internal/modules/authorization/repo.go | 2 -- .../modules/authorization/types/user-model.go | 23 ++++++++++--------- .../modules/user/types/profile-pic-dto.go | 5 ---- backend/internal/routes/router.go | 12 ++++++++++ backend/pkg/utils/file.go | 6 +++-- backend/pkg/utils/logger.go | 10 ++++---- 9 files changed, 45 insertions(+), 27 deletions(-) delete mode 100644 backend/internal/modules/user/types/profile-pic-dto.go diff --git a/backend/.gitignore b/backend/.gitignore index 315b7cd..f4d1762 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -9,4 +9,5 @@ .vscode .DS_Store .idea -.env \ No newline at end of file +.env +/uploads/ \ No newline at end of file diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 4780adf..a8b1a47 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -6,6 +6,7 @@ import ( "takumi/internal/config" "takumi/internal/database" "takumi/internal/modules/authorization" + "takumi/internal/modules/user" "takumi/internal/routes" "github.com/gin-gonic/gin" @@ -30,6 +31,14 @@ func main() { authHandlers := authorization.NewHandler(authService) router.RegisterAuthRoutes(authHandlers) + userService, err := user.InitUserService(dbHandler) + if err != nil { + log.Fatal("error initializing user service: ", err) + return + } + userHandlers := user.NewHandler(userService) + router.RegisterUserRoutes(userHandlers) + err = app.Run(":" + cfg.Port) if err != nil { log.Fatal("error running server: ", err) diff --git a/backend/internal/database/postgres.go b/backend/internal/database/postgres.go index 3c362ac..b659293 100644 --- a/backend/internal/database/postgres.go +++ b/backend/internal/database/postgres.go @@ -2,7 +2,7 @@ package database import ( "log" - "takumi/internal/modules/authorization/types" + "takumi/internal/modules/user/types" "gorm.io/driver/postgres" "gorm.io/gorm" diff --git a/backend/internal/modules/authorization/repo.go b/backend/internal/modules/authorization/repo.go index 1ee98dc..09b475e 100644 --- a/backend/internal/modules/authorization/repo.go +++ b/backend/internal/modules/authorization/repo.go @@ -51,9 +51,7 @@ func CreateUser(registration *types.User, handler *database.DBHandler) (*types.U Password: password, Gender: registration.Gender, BirthDate: registration.BirthDate, - Role: "USER", CreatedAt: time.Now(), - Coins: 0, } err = handler.DB.Create(user).Error diff --git a/backend/internal/modules/authorization/types/user-model.go b/backend/internal/modules/authorization/types/user-model.go index e3d3ecb..65766fb 100644 --- a/backend/internal/modules/authorization/types/user-model.go +++ b/backend/internal/modules/authorization/types/user-model.go @@ -3,15 +3,16 @@ package types import "time" type User struct { - ID int `gorm:"primaryKey;autoIncrement" json:"id"` - Username string `gorm:"unique;not null" json:"username"` - FirstName string `gorm:"not null" json:"firstName"` - LastName string `gorm:"not null" json:"lastName"` - Email string `gorm:"unique;not null" json:"email"` - Gender string `json:"gender"` - BirthDate time.Time `json:"birthDate"` - Password string `gorm:"not null" json:"password"` - Role string `gorm:"default:USER" json:"role"` - CreatedAt time.Time `json:"createdAt"` - Coins int `gorm:"default:0" json:"coins"` + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + Username string `gorm:"unique;not null" json:"username"` + FirstName string `gorm:"not null" json:"firstName"` + LastName string `gorm:"not null" json:"lastName"` + Email string `gorm:"unique;not null" json:"email"` + Gender string `json:"gender"` + BirthDate time.Time `json:"birthDate"` + Password string `gorm:"not null" json:"password"` + Role string `gorm:"default:USER" json:"role"` + CreatedAt time.Time `json:"createdAt"` + Coins int `gorm:"default:0" json:"coins"` + ProfilePicture string `gorm:"default:''" json:"profilePicture"` } diff --git a/backend/internal/modules/user/types/profile-pic-dto.go b/backend/internal/modules/user/types/profile-pic-dto.go deleted file mode 100644 index 382dc7f..0000000 --- a/backend/internal/modules/user/types/profile-pic-dto.go +++ /dev/null @@ -1,5 +0,0 @@ -package types - -type ProfilePicReq struct { - ProfilePictureURL string `json:"profilePicURL" binding:"required"` -} diff --git a/backend/internal/routes/router.go b/backend/internal/routes/router.go index 78fde8f..e7ec9fe 100644 --- a/backend/internal/routes/router.go +++ b/backend/internal/routes/router.go @@ -2,6 +2,7 @@ package routes import ( "takumi/internal/modules/authorization" + "takumi/internal/modules/user" "github.com/gin-gonic/gin" ) @@ -28,3 +29,14 @@ func (tr *TakumiRouter) RegisterAuthRoutes(handler *authorization.Handler) { router.POST("/signup", handler.SignUpHandler) router.GET("/current-user", handler.GetCurrentUser) } + +func (tr *TakumiRouter) RegisterUserRoutes(handler *user.Handler) { + router := tr.Routes.Group("/user") + + router.GET("/:id", handler.GetUserByIDHandler) + router.DELETE("/:id", handler.DeleteUserByIDHandler) + router.PUT("/update", handler.UpdateUserParamsHandler) + router.POST("/:id/profile-picture", handler.UpdateProfilePictureHandler) + router.GET("/:id/profile-picture", handler.GetProfilePictureByUserID) + router.DELETE("/:id/profile-picture", handler.DeleteProfilePictureHandler) +} diff --git a/backend/pkg/utils/file.go b/backend/pkg/utils/file.go index a820191..a0a8f72 100644 --- a/backend/pkg/utils/file.go +++ b/backend/pkg/utils/file.go @@ -7,8 +7,10 @@ import ( "path/filepath" ) +const FilesDir = "uploads/" + func DeleteFile(branch, filePath string) error { - fullPath := filepath.Join("uploads/", branch, filePath) + fullPath := filepath.Join(FilesDir, branch, filePath) if _, err := os.Stat(fullPath); err == nil { if err := os.Remove(fullPath); err != nil { @@ -24,7 +26,7 @@ func DeleteFile(branch, filePath string) error { } func SaveFile(file io.Reader, branch, filePath string) error { - fullPath := filepath.Join("uploads/", branch) + fullPath := filepath.Join(FilesDir, branch) if err := os.MkdirAll(fullPath, os.ModePerm); err != nil { return fmt.Errorf("failed to create directory %s: %w", fullPath, err) } diff --git a/backend/pkg/utils/logger.go b/backend/pkg/utils/logger.go index 10eecda..5c5da06 100644 --- a/backend/pkg/utils/logger.go +++ b/backend/pkg/utils/logger.go @@ -7,22 +7,22 @@ import ( "time" ) -type CustomLogger struct { +type TakumiLogger struct { logger *log.Logger } -func NewLogger() *CustomLogger { - return &CustomLogger{ +func NewLogger() *TakumiLogger { + return &TakumiLogger{ logger: log.New(os.Stdout, "", log.LstdFlags), } } -func (l *CustomLogger) LogError(message string, err error, details map[string]interface{}) { +func (l *TakumiLogger) LogError(message string, err error, details map[string]interface{}) { logMsg := fmt.Sprintf("%s | ERROR: %s | DETAILS: %v | %s", time.Now().Format(time.RFC3339), message, details, err) l.logger.Println(logMsg) } -func (l *CustomLogger) LogInfo(message string, details map[string]interface{}) { +func (l *TakumiLogger) LogInfo(message string, details map[string]interface{}) { logMsg := fmt.Sprintf("%s | INFO: %s | DETAILS: %v", time.Now().Format(time.RFC3339), message, details) l.logger.Println(logMsg) } From 595ebcf5368fed4b17d65ff8ef79dbe8f8cd76ce Mon Sep 17 00:00:00 2001 From: maulerrr <211239@astanait.edu.kz> Date: Mon, 26 Aug 2024 03:32:52 +0500 Subject: [PATCH 5/8] Update .gitignore --- backend/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/.gitignore b/backend/.gitignore index f4d1762..bb2cd17 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -10,4 +10,5 @@ .DS_Store .idea .env -/uploads/ \ No newline at end of file +/uploads/ +/test/ \ No newline at end of file From 46a6a04306493afb695142bcf050488a011e3304 Mon Sep 17 00:00:00 2001 From: maulerrr <211239@astanait.edu.kz> Date: Mon, 26 Aug 2024 03:47:33 +0500 Subject: [PATCH 6/8] feature: generalized module initialization now code is more clear ig --- backend/cmd/main.go | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index a8b1a47..bac267b 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -23,21 +23,8 @@ func main() { app.Static("/uploads", "./uploads") router := routes.NewTakumiRouter(app, "/api", "/v1") - authService, err := authorization.InitAuthService(dbHandler) - if err != nil { - log.Fatal("error initializing authorization service: ", err) - return - } - authHandlers := authorization.NewHandler(authService) - router.RegisterAuthRoutes(authHandlers) - - userService, err := user.InitUserService(dbHandler) - if err != nil { - log.Fatal("error initializing user service: ", err) - return - } - userHandlers := user.NewHandler(userService) - router.RegisterUserRoutes(userHandlers) + InitializeModule(dbHandler, authorization.InitAuthService, authorization.NewHandler, router.RegisterAuthRoutes) + InitializeModule(dbHandler, user.InitUserService, user.NewHandler, router.RegisterUserRoutes) err = app.Run(":" + cfg.Port) if err != nil { @@ -63,3 +50,22 @@ func setupGin(cfg *config.Config) *gin.Engine { return app } + +type Service interface{} +type Handler interface{} + +func InitializeModule[T Service, H Handler]( + dbHandler database.DBHandler, + initService func(dbHandler database.DBHandler) (T, error), + createHandler func(T) H, + registerRoutes func(H)) { + service, err := initService(dbHandler) + if err != nil { + log.Fatalf("error initializing service: %v", err) + return + } + + handler := createHandler(service) + + registerRoutes(handler) +} From d5445e8cb06e0b71291a153f52407c49f08173db Mon Sep 17 00:00:00 2001 From: maulerrr <211239@astanait.edu.kz> Date: Wed, 28 Aug 2024 02:36:51 +0500 Subject: [PATCH 7/8] feature: deletion of user profile picture fixed --- backend/internal/modules/user/handler.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/internal/modules/user/handler.go b/backend/internal/modules/user/handler.go index 31ab9cf..57a5749 100644 --- a/backend/internal/modules/user/handler.go +++ b/backend/internal/modules/user/handler.go @@ -129,6 +129,13 @@ func (h *Handler) DeleteProfilePictureHandler(c *gin.Context) { } deletedUser, err := h.Service.DeleteProfilePicture(c, userID) + + fileName := fmt.Sprintf("user_%d_profile_pic.jpg", userID) + if err := utils.DeleteFile("profile-pictures", fileName); err != nil { + log.Printf("Failed to delete: %s, error: %v", fileName, err) + return + } + if err != nil { utils.SendMessageWithStatus(c, err.Error(), http.StatusInternalServerError) return From ada3114dae336df3480385b0507828290693321b Mon Sep 17 00:00:00 2001 From: maulerrr <211239@astanait.edu.kz> Date: Wed, 28 Aug 2024 03:05:27 +0500 Subject: [PATCH 8/8] feature: added PATCH update for user params for flexibility --- backend/api/cors.go | 2 +- backend/internal/modules/user/handler.go | 22 ++++++++++++++++++++++ backend/internal/modules/user/repo.go | 14 ++++++++++++++ backend/internal/modules/user/service.go | 13 +++++++++++++ backend/internal/routes/router.go | 1 + 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/backend/api/cors.go b/backend/api/cors.go index e837cd5..301eea1 100644 --- a/backend/api/cors.go +++ b/backend/api/cors.go @@ -10,7 +10,7 @@ import ( func Config() gin.HandlerFunc { return cors.New(cors.Config{ AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, diff --git a/backend/internal/modules/user/handler.go b/backend/internal/modules/user/handler.go index 57a5749..ab77531 100644 --- a/backend/internal/modules/user/handler.go +++ b/backend/internal/modules/user/handler.go @@ -70,6 +70,28 @@ func (h *Handler) UpdateUserParamsHandler(c *gin.Context) { utils.SendSuccessJSON(c, updatedUser) } +func (h *Handler) PatchUserParamsHandler(c *gin.Context) { + userID, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: Invalid user ID", http.StatusBadRequest) + return + } + + updateData := map[string]interface{}{} + if err := c.ShouldBindJSON(&updateData); err != nil { + utils.SendMessageWithStatus(c, "ERROR: Invalid request body", http.StatusBadRequest) + return + } + + updatedUser, err := h.Service.PatchUserParams(c, userID, updateData) + if err != nil { + utils.SendMessageWithStatus(c, "ERROR: "+err.Error(), http.StatusInternalServerError) + return + } + + utils.SendSuccessJSON(c, updatedUser) +} + func (h *Handler) UpdateProfilePictureHandler(c *gin.Context) { userIDParam := c.Param("id") userID, err := strconv.Atoi(userIDParam) diff --git a/backend/internal/modules/user/repo.go b/backend/internal/modules/user/repo.go index 30e5cd4..f7a96a2 100644 --- a/backend/internal/modules/user/repo.go +++ b/backend/internal/modules/user/repo.go @@ -46,6 +46,20 @@ func UpdateUserParams(update types.User, handler database.DBHandler) (*types.Use return &user, nil } +func PatchUserParams(userID int, updateData map[string]interface{}, handler database.DBHandler) (*types.User, error) { + user := types.User{} + + if err := handler.DB.First(&user, userID).Error; err != nil { + return nil, err + } + + if err := handler.DB.Model(&user).Updates(updateData).Error; err != nil { + return nil, err + } + + return &user, nil +} + func UpdateUserProfilePicture(userID int, profilePictureURL string, handler database.DBHandler) (*types.User, error) { user := types.User{} tx := handler.DB.Begin() diff --git a/backend/internal/modules/user/service.go b/backend/internal/modules/user/service.go index b189b48..c38ea04 100644 --- a/backend/internal/modules/user/service.go +++ b/backend/internal/modules/user/service.go @@ -59,6 +59,19 @@ func (s *Service) UpdateUserParams(c *gin.Context, update types.User) (*types.Us return user, nil } +func (s *Service) PatchUserParams(c *gin.Context, userID int, updateData map[string]interface{}) (*types.User, error) { + user, err := PatchUserParams(userID, updateData, s.Handler) + if err != nil { + if err == gorm.ErrRecordNotFound { + c.AbortWithError(http.StatusNotFound, errors.New("user not found")) + return nil, errors.New("user not found") + } + c.AbortWithError(http.StatusInternalServerError, err) + return nil, errors.New("failed to update user") + } + return user, nil +} + func (s *Service) UpdateProfilePicture(c *gin.Context, userID int, profilePictureURL string) (*types.User, error) { user, err := UpdateUserProfilePicture(userID, profilePictureURL, s.Handler) if err != nil { diff --git a/backend/internal/routes/router.go b/backend/internal/routes/router.go index e7ec9fe..fb1d2b0 100644 --- a/backend/internal/routes/router.go +++ b/backend/internal/routes/router.go @@ -36,6 +36,7 @@ func (tr *TakumiRouter) RegisterUserRoutes(handler *user.Handler) { router.GET("/:id", handler.GetUserByIDHandler) router.DELETE("/:id", handler.DeleteUserByIDHandler) router.PUT("/update", handler.UpdateUserParamsHandler) + router.PATCH("/update/:id", handler.PatchUserParamsHandler) router.POST("/:id/profile-picture", handler.UpdateProfilePictureHandler) router.GET("/:id/profile-picture", handler.GetProfilePictureByUserID) router.DELETE("/:id/profile-picture", handler.DeleteProfilePictureHandler)