diff --git a/api/server.go b/api/server.go index 296f42f..5cdd01f 100644 --- a/api/server.go +++ b/api/server.go @@ -67,8 +67,11 @@ func (server *Server) getRoutes() { // REQUIRE AUTH TOKEN authRoutes := router.Use(authMiddleware(server.TokenMaker)) authRoutes.POST("/review/location", server.createReview) + authRoutes.PATCH("/user", server.updateUser) authRoutes.GET("/user/review/location/:location_id", server.getUserReviewByLocation) authRoutes.GET("/user/profile", server.getUserStats) + authRoutes.PATCH("/user/avatar", server.updateUserAvatar) + authRoutes.DELETE("/user/avatar", server.removeAvatar) server.Router = router } diff --git a/api/user.go b/api/user.go index 8a9162d..df42c1e 100644 --- a/api/user.go +++ b/api/user.go @@ -3,7 +3,10 @@ package api import ( "database/sql" "encoding/json" + "fmt" "net/http" + "os" + "path/filepath" "time" db "git.nochill.in/nochill/hiling_go/db/sqlc" @@ -11,6 +14,7 @@ import ( "git.nochill.in/nochill/hiling_go/util/token" "github.com/gin-gonic/gin" "github.com/lib/pq" + "github.com/sqlc-dev/pqtype" ) type createUserRequest struct { @@ -161,6 +165,99 @@ func (server *Server) getUserStats(ctx *gin.Context) { }) } +type UpdateUserRequest struct { + About string `json:"about"` + Website string `json:"website"` + SocialMedia []map[string]string `json:"social_media"` +} + +func (server *Server) updateUser(ctx *gin.Context) { + var req UpdateUserRequest + + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) + return + } + + authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) + + fmt.Println(req.About) + + social, _ := json.Marshal(req.SocialMedia) + socialArr := pqtype.NullRawMessage{ + RawMessage: social, + Valid: len(req.SocialMedia) > 0, + } + + user, err := server.Store.UpdateUser(ctx, db.UpdateUserParams{ + About: sql.NullString{String: req.About, Valid: true}, + SocialMedia: socialArr, + Website: sql.NullString{String: req.Website, Valid: len(req.Website) > 0}, + ID: int32(authPayload.UserID), + }) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to update user")) + return + } + + ctx.JSON(http.StatusOK, user) +} + +func (server *Server) updateUserAvatar(ctx *gin.Context) { + file, err := ctx.FormFile("file") + + if err != nil { + ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to parse file")) + return + } + + authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) + + fileExt := filepath.Ext(file.Filename) + now := time.Now() + dir := fmt.Sprintf("public/upload/images/user/%d/avatar", authPayload.UserID) + osFilename := fmt.Sprintf("%s%s%s", util.RandomString(5), fmt.Sprintf("%v", now.Unix()), fileExt) + imgPath := fmt.Sprintf("%s/%s", dir, osFilename) + + if _, err := os.Stat(dir); os.IsNotExist(err) { + os.Mkdir(dir, 0775) + } + + if err := ctx.SaveUploadedFile(file, imgPath); err != nil { + ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Error while try to save thumbnail image")) + return + } + + url, err := server.Store.UpdateAvatar(ctx, db.UpdateAvatarParams{ + ID: int32(authPayload.UserID), + AvatarPicture: sql.NullString{Valid: true, String: imgPath}, + }) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to save image to database")) + return + } + + ctx.JSON(http.StatusOK, gin.H{"image_url": url.String}) +} + +func (server *Server) removeAvatar(ctx *gin.Context) { + authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload) + + _, err := server.Store.UpdateAvatar(ctx, db.UpdateAvatarParams{ + AvatarPicture: sql.NullString{String: "", Valid: false}, + ID: int32(authPayload.UserID), + }) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to update user avatar")) + return + } + + ctx.Writer.WriteHeader(http.StatusNoContent) +} + func (server *Server) login(ctx *gin.Context) { var req createUserRequest diff --git a/db/mock/store.go b/db/mock/store.go index 8f7c32e..cdc4362 100644 --- a/db/mock/store.go +++ b/db/mock/store.go @@ -6,6 +6,7 @@ package mockdb import ( context "context" + sql "database/sql" reflect "reflect" db "git.nochill.in/nochill/hiling_go/db/sqlc" @@ -391,6 +392,21 @@ func (mr *MockStoreMockRecorder) RemoveFollowUser(arg0, arg1 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveFollowUser", reflect.TypeOf((*MockStore)(nil).RemoveFollowUser), arg0, arg1) } +// UpdateAvatar mocks base method. +func (m *MockStore) UpdateAvatar(arg0 context.Context, arg1 db.UpdateAvatarParams) (sql.NullString, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateAvatar", arg0, arg1) + ret0, _ := ret[0].(sql.NullString) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateAvatar indicates an expected call of UpdateAvatar. +func (mr *MockStoreMockRecorder) UpdateAvatar(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAvatar", reflect.TypeOf((*MockStore)(nil).UpdateAvatar), arg0, arg1) +} + // UpdateLocationThumbnail mocks base method. func (m *MockStore) UpdateLocationThumbnail(arg0 context.Context, arg1 db.UpdateLocationThumbnailParams) error { m.ctrl.T.Helper() @@ -420,10 +436,10 @@ func (mr *MockStoreMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock. } // UpdateUser mocks base method. -func (m *MockStore) UpdateUser(arg0 context.Context, arg1 db.UpdateUserParams) (db.User, error) { +func (m *MockStore) UpdateUser(arg0 context.Context, arg1 db.UpdateUserParams) (db.UpdateUserRow, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateUser", arg0, arg1) - ret0, _ := ret[0].(db.User) + ret0, _ := ret[0].(db.UpdateUserRow) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/db/queries/users.sql b/db/queries/users.sql index b953d44..6997a6c 100644 --- a/db/queries/users.sql +++ b/db/queries/users.sql @@ -5,17 +5,13 @@ INSERT INTO users ( ) VALUES ($1, $2) RETURNING *; --- name: UpdateUser :one -UPDATE users -SET - email = COALESCE(sqlc.narg(email), email), - username = COALESCE(sqlc.narg(username), username), - avatar_picture = COALESCE(sqlc.narg(avatar_picture), avatar_picture) -WHERE - id = sqlc.arg(id) -RETURNING *; - -- name: UpdatePassword :exec UPDATE users SET password = $1 WHERE id = $2; + +-- name: UpdateAvatar :one +UPDATE users +SET avatar_picture = $1 +WHERE id = $2 +RETURNING avatar_picture; diff --git a/db/sqlc/models.go b/db/sqlc/models.go index 0f15740..b677b16 100644 --- a/db/sqlc/models.go +++ b/db/sqlc/models.go @@ -269,6 +269,8 @@ type User struct { SocialMedia pqtype.NullRawMessage `json:"social_media"` CreatedAt sql.NullTime `json:"created_at"` UpdatedAt sql.NullTime `json:"updated_at"` + About sql.NullString `json:"about"` + Website sql.NullString `json:"website"` } type UserActivity struct { diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index dc3b2aa..c3dd22d 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -6,6 +6,7 @@ package db import ( "context" + "database/sql" ) type Querier interface { @@ -23,9 +24,9 @@ type Querier interface { GetSession(ctx context.Context, id int32) (UserSession, error) GetUserReviewByLocation(ctx context.Context, arg GetUserReviewByLocationParams) (GetUserReviewByLocationRow, error) RemoveFollowUser(ctx context.Context, arg RemoveFollowUserParams) error + UpdateAvatar(ctx context.Context, arg UpdateAvatarParams) (sql.NullString, error) UpdateLocationThumbnail(ctx context.Context, arg UpdateLocationThumbnailParams) error UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error - UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) } var _ Querier = (*Queries)(nil) diff --git a/db/sqlc/store.go b/db/sqlc/store.go index a43317f..701151c 100644 --- a/db/sqlc/store.go +++ b/db/sqlc/store.go @@ -14,6 +14,7 @@ type Store interface { GetImagesByLocation(ctx context.Context, arg GetImagesByLocationParams) ([]GetImagesByLocationRow, error) GetLocation(ctx context.Context, location_id int32) (GetLocationRow, error) GetUser(ctx context.Context, username string) (GetUserRow, error) + UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) GetUserStats(ctx context.Context, user_id int32) (GetUserStatsRow, error) CreateReview(ctx context.Context, arg CreateReviewParams) (Review, error) GetListLocationReviews(ctx context.Context, arg GetListLocationReviewsParams) ([]GetListLocationReviewsRow, error) diff --git a/db/sqlc/users.go b/db/sqlc/users.go index 7722cb2..1ca55c0 100644 --- a/db/sqlc/users.go +++ b/db/sqlc/users.go @@ -4,7 +4,7 @@ import ( "context" "database/sql" "encoding/json" - "fmt" + "time" "github.com/sqlc-dev/pqtype" ) @@ -15,6 +15,9 @@ SELECT COALESCE(email, '') as email, password, username, + COALESCE(google_sign_in_payload, '') as google_sign_in_payload, + COALESCE(about, '') as about, + COALESCE(website, '') as website, COALESCE(avatar_picture, '') as avatar_picture, banned_at, banned_until, @@ -23,25 +26,32 @@ SELECT is_admin, is_critics, is_verified, - social_media + COALESCE(social_media, '[]'), + created_at, + updated_at FROM USERS WHERE username = $1 ` type GetUserRow struct { - ID int32 `json:"id"` - Email string `json:"email"` - Password string `json:"-"` - Username string `json:"username"` - AvatarPicture string `json:"avatar_picture"` - BannedAt sql.NullTime `json:"banned_at"` - BannedUntil sql.NullTime `json:"banned_until"` - BanReason string `json:"ban_reason"` - IsPermaban bool `json:"is_permaban"` - IsAdmin bool `json:"is_admin"` - IsCritics bool `json:"is_critics"` - IsVerified bool `json:"is_verified"` - SocialMedia pqtype.NullRawMessage `json:"social_media"` + ID int32 `json:"id"` + Email string `json:"email"` + Password string `json:"-"` + About string `json:"about"` + Website string `json:"website"` + Username string `json:"username"` + GoogleSignInPayload string `json:"google_sign_in_payload"` + AvatarPicture string `json:"avatar_picture"` + BannedAt sql.NullTime `json:"banned_at"` + BannedUntil sql.NullTime `json:"banned_until"` + BanReason string `json:"ban_reason"` + IsPermaban bool `json:"is_permaban"` + IsAdmin bool `json:"is_admin"` + IsCritics bool `json:"is_critics"` + IsVerified bool `json:"is_verified"` + SocialMedia json.RawMessage `json:"social_media"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (q *Queries) GetUser(ctx context.Context, username string) (GetUserRow, error) { @@ -52,6 +62,9 @@ func (q *Queries) GetUser(ctx context.Context, username string) (GetUserRow, err &i.Email, &i.Password, &i.Username, + &i.GoogleSignInPayload, + &i.About, + &i.Website, &i.AvatarPicture, &i.BannedAt, &i.BannedUntil, @@ -61,6 +74,8 @@ func (q *Queries) GetUser(ctx context.Context, username string) (GetUserRow, err &i.IsCritics, &i.IsVerified, &i.SocialMedia, + &i.CreatedAt, + &i.UpdatedAt, ) return i, err } @@ -126,12 +141,94 @@ func (q *Queries) GetUserStats(ctx context.Context, user_id int32) (GetUserStats &i.ScoreCount, &i.ScoresDistribution, ) - - var r []map[string]any - - err = json.Unmarshal(i.ScoresDistribution, &r) - fmt.Println(r) - return i, err } + +const updateUser = `-- name: UpdateUser :one +UPDATE users +SET + about = $1, + social_media = $2, + website = $3 +WHERE + id = $4 +RETURNING + id, + COALESCE(email, ''), + username, + COALESCE(avatar_picture, ''), + COALESCE(about, ''), + COALESCE(website, ''), + COALESCE(google_sign_in_payload, ''), + banned_at, + banned_until, + COALESCE(ban_reason, ''), + is_permaban, + is_admin, + is_critics, + is_verified, + is_active, + COALESCE(social_media, '[]'), + created_at, + updated_at +` + +type UpdateUserParams struct { + About sql.NullString `json:"about"` + SocialMedia pqtype.NullRawMessage `json:"social_media"` + Website sql.NullString `json:"website"` + ID int32 `json:"id"` +} + +type UpdateUserRow struct { + ID int32 `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + AvatarPicture string `json:"avatar_picture"` + About string `json:"about"` + Website string `json:"website"` + GoogleSignInPayload string `json:"google_sign_in_payload"` + BannedAt sql.NullTime `json:"banned_at"` + BannedUntil sql.NullTime `json:"banned_until"` + BanReason string `json:"ban_reason"` + IsPermaban bool `json:"is_permaban"` + IsAdmin bool `json:"is_admin"` + IsCritics bool `json:"is_critics"` + IsVerified bool `json:"is_verified"` + IsActive bool `json:"is_active"` + SocialMedia json.RawMessage `json:"social_media"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) { + row := q.db.QueryRowContext(ctx, updateUser, + arg.About, + arg.SocialMedia, + arg.Website, + arg.ID, + ) + var i UpdateUserRow + err := row.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.AvatarPicture, + &i.About, + &i.Website, + &i.GoogleSignInPayload, + &i.BannedAt, + &i.BannedUntil, + &i.BanReason, + &i.IsPermaban, + &i.IsAdmin, + &i.IsCritics, + &i.IsVerified, + &i.IsActive, + &i.SocialMedia, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/db/sqlc/users.sql.go b/db/sqlc/users.sql.go index 6dbb453..f534e4d 100644 --- a/db/sqlc/users.sql.go +++ b/db/sqlc/users.sql.go @@ -15,7 +15,7 @@ INSERT INTO users ( username, password ) VALUES ($1, $2) -RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, is_active, social_media, created_at, updated_at +RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, is_active, social_media, created_at, updated_at, about, website ` type CreateUserParams struct { @@ -44,10 +44,31 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.SocialMedia, &i.CreatedAt, &i.UpdatedAt, + &i.About, + &i.Website, ) return i, err } +const updateAvatar = `-- name: UpdateAvatar :one +UPDATE users +SET avatar_picture = $1 +WHERE id = $2 +RETURNING avatar_picture +` + +type UpdateAvatarParams struct { + AvatarPicture sql.NullString `json:"avatar_picture"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateAvatar(ctx context.Context, arg UpdateAvatarParams) (sql.NullString, error) { + row := q.db.QueryRowContext(ctx, updateAvatar, arg.AvatarPicture, arg.ID) + var avatar_picture sql.NullString + err := row.Scan(&avatar_picture) + return avatar_picture, err +} + const updatePassword = `-- name: UpdatePassword :exec UPDATE users SET password = $1 @@ -63,51 +84,3 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) _, err := q.db.ExecContext(ctx, updatePassword, arg.Password, arg.ID) return err } - -const updateUser = `-- name: UpdateUser :one -UPDATE users -SET - email = COALESCE($1, email), - username = COALESCE($2, username), - avatar_picture = COALESCE($3, avatar_picture) -WHERE - id = $4 -RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, is_active, social_media, created_at, updated_at -` - -type UpdateUserParams struct { - Email sql.NullString `json:"email"` - Username sql.NullString `json:"username"` - AvatarPicture sql.NullString `json:"avatar_picture"` - ID int32 `json:"id"` -} - -func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) { - row := q.db.QueryRowContext(ctx, updateUser, - arg.Email, - arg.Username, - arg.AvatarPicture, - arg.ID, - ) - var i User - err := row.Scan( - &i.ID, - &i.Email, - &i.Username, - &i.Password, - &i.AvatarPicture, - &i.GoogleSignInPayload, - &i.BannedAt, - &i.BannedUntil, - &i.BanReason, - &i.IsPermaban, - &i.IsAdmin, - &i.IsCritics, - &i.IsVerified, - &i.IsActive, - &i.SocialMedia, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -}