From d14f90d628b719075bbbdc132253f5d0657f81c5 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Wed, 23 Jul 2025 17:46:59 +0300 Subject: [PATCH] feat: complete profile update and settings management refactor: change profile update endpoints to PUT refactor: changed profile settings update query to use username chore: update SQL queries for profile operations --- backend/docs/docs.go | 4 +- backend/docs/swagger.json | 4 +- backend/docs/swagger.yaml | 4 +- backend/internal/controllers/profile.go | 8 +-- backend/internal/database/query.sql.go | 32 +++++---- backend/internal/services/profile.go | 94 ++++++++++++++++++++++++- sqlc/query.sql | 22 +++--- 7 files changed, 130 insertions(+), 38 deletions(-) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index a72bbf8..7686b96 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -295,7 +295,7 @@ const docTemplate = `{ } } }, - "patch": { + "put": { "security": [ { "JWT": [] @@ -394,7 +394,7 @@ const docTemplate = `{ } } }, - "patch": { + "put": { "security": [ { "JWT": [] diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index dd6fb1f..8cde3c8 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -291,7 +291,7 @@ } } }, - "patch": { + "put": { "security": [ { "JWT": [] @@ -390,7 +390,7 @@ } } }, - "patch": { + "put": { "security": [ { "JWT": [] diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 6ee6635..87f9a88 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -342,7 +342,7 @@ paths: summary: Get your profile tags: - Profile - patch: + put: consumes: - application/json parameters: @@ -428,7 +428,7 @@ paths: summary: Get your profile settings tags: - Profile - patch: + put: consumes: - application/json parameters: diff --git a/backend/internal/controllers/profile.go b/backend/internal/controllers/profile.go index 73c25a9..b998e68 100644 --- a/backend/internal/controllers/profile.go +++ b/backend/internal/controllers/profile.go @@ -65,14 +65,14 @@ func NewProfileController(_log *zap.Logger, _ps services.ProfileService) Control Function: ctrl.getProfileSettings, }, { - HttpMethod: PATCH, + HttpMethod: PUT, Path: "", Authorization: enums.UserRole, Middleware: []gin.HandlerFunc{}, Function: ctrl.updateProfile, }, { - HttpMethod: PATCH, + HttpMethod: PUT, Path: "/settings", Authorization: enums.UserRole, Middleware: []gin.HandlerFunc{}, @@ -171,7 +171,7 @@ func (ctrl *ProfileController) getProfileSettings(c *gin.Context) { // @Security JWT // @Param request body dto.ProfileDto true " " // @Success 200 {object} bool " " -// @Router /profile [patch] +// @Router /profile [put] func (ctrl *ProfileController) updateProfile(c *gin.Context) { request, err := GetRequest[dto.ProfileDto](c); if err != nil { return @@ -193,7 +193,7 @@ func (ctrl *ProfileController) updateProfile(c *gin.Context) { // @Security JWT // @Param request body dto.ProfileSettingsDto true " " // @Success 200 {object} bool " " -// @Router /profile/settings [patch] +// @Router /profile/settings [put] func (ctrl *ProfileController) updateProfileSettings(c *gin.Context) { request, err := GetRequest[dto.ProfileSettingsDto](c); if err != nil { return diff --git a/backend/internal/database/query.sql.go b/backend/internal/database/query.sql.go index 7847220..3ce6d70 100644 --- a/backend/internal/database/query.sql.go +++ b/backend/internal/database/query.sql.go @@ -1020,21 +1020,23 @@ func (q *Queries) UpdateProfileByUsername(ctx context.Context, arg UpdateProfile return err } -const updateProfileSettings = `-- name: UpdateProfileSettings :exec -UPDATE profile_settings +const updateProfileSettingsByUsername = `-- name: UpdateProfileSettingsByUsername :exec +UPDATE profile_settings ps SET - hide_fulfilled = COALESCE($2, hide_fulfilled), - hide_profile_details = COALESCE($3, hide_profile_details), - hide_for_unauthenticated = COALESCE($4, hide_for_unauthenticated), - hide_birthday = COALESCE($5, hide_birthday), - hide_dates = COALESCE($6, hide_dates), - captcha = COALESCE($7, captcha), - followers_only_interaction = COALESCE($8, followers_only_interaction) -WHERE id = $1 + hide_fulfilled = COALESCE($2, ps.hide_fulfilled), + hide_profile_details = COALESCE($3, ps.hide_profile_details), + hide_for_unauthenticated = COALESCE($4, ps.hide_for_unauthenticated), + hide_birthday = COALESCE($5, ps.hide_birthday), + hide_dates = COALESCE($6, ps.hide_dates), + captcha = COALESCE($7, ps.captcha), + followers_only_interaction = COALESCE($8, ps.followers_only_interaction) +FROM profiles p +JOIN users u ON p.user_id = u.id +WHERE ps.profile_id = p.id AND u.username = $1 ` -type UpdateProfileSettingsParams struct { - ID int64 +type UpdateProfileSettingsByUsernameParams struct { + Username string HideFulfilled bool HideProfileDetails bool HideForUnauthenticated bool @@ -1044,9 +1046,9 @@ type UpdateProfileSettingsParams struct { FollowersOnlyInteraction bool } -func (q *Queries) UpdateProfileSettings(ctx context.Context, arg UpdateProfileSettingsParams) error { - _, err := q.db.Exec(ctx, updateProfileSettings, - arg.ID, +func (q *Queries) UpdateProfileSettingsByUsername(ctx context.Context, arg UpdateProfileSettingsByUsernameParams) error { + _, err := q.db.Exec(ctx, updateProfileSettingsByUsername, + arg.Username, arg.HideFulfilled, arg.HideProfileDetails, arg.HideForUnauthenticated, diff --git a/backend/internal/services/profile.go b/backend/internal/services/profile.go index 9ea673b..291f68e 100644 --- a/backend/internal/services/profile.go +++ b/backend/internal/services/profile.go @@ -23,9 +23,12 @@ import ( errs "easywish/internal/errors" mapspecial "easywish/internal/utils/mapSpecial" "errors" + "time" "github.com/go-redis/redis/v8" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/rafiulgits/go-automapper" "go.uber.org/zap" ) @@ -65,6 +68,7 @@ func (p *profileServiceImpl) GetMyProfile(cinfo dto.ClientInfo) (*dto.ProfileDto return &profileDto, nil } +// XXX: untested // TODO: Profile privacy settings checks func (p *profileServiceImpl) GetProfileByUsername(cinfo dto.ClientInfo, username string) (*dto.ProfileDto, error) { helper, db, err := database.NewDbHelperTransaction(p.dbctx); if err != nil { @@ -93,18 +97,102 @@ func (p *profileServiceImpl) GetProfileByUsername(cinfo dto.ClientInfo, username return &profileDto, nil } +// XXX: unstested func (p *profileServiceImpl) GetProfileSettings(cinfo dto.ClientInfo) (*dto.ProfileSettingsDto, error) { - panic("unimplemented") + db := database.NewDbHelper(p.dbctx); + + profileSettings, err := db.Queries.GetProfileSettingsByUsername(db.CTX, cinfo.Username); if err != nil { + p.log.Error( + "Failed to find user profile settings by username", + zap.Error(err)) + return nil, errs.ErrServerError + } + + var profileSettingsDto dto.ProfileSettingsDto + automapper.Map(profileSettings, profileSettingsDto) + + return &profileSettingsDto, nil } +// XXX: no validation for timestamps' allowed ranges func (p *profileServiceImpl) UpdateProfile(cinfo dto.ClientInfo, newProfile dto.ProfileDto) (bool, error) { - panic("unimplemented") + helper, db, err := database.NewDbHelperTransaction(p.dbctx); if err != nil { + p.log.Error( + "Failed to open transaction", + zap.Error(err)) + return false, err + } + defer helper.Rollback() + + birthdayTimestamp := pgtype.Timestamp { + Time: time.UnixMilli(newProfile.Birthday), + Valid: true, + } + + err = db.TXlessQueries.UpdateProfileByUsername(db.CTX, database.UpdateProfileByUsernameParams{ + Username: cinfo.Username, + Name: newProfile.Name, + Bio: newProfile.Bio, + Birthday: birthdayTimestamp, + }); if err != nil { + p.log.Error( + "Failed to update user profile", + zap.String("username", cinfo.Username), + zap.Error(err)) + return false, errs.ErrServerError + } + + err = helper.Commit(); if err != nil { + p.log.Error( + "Failed to commit transaction", + zap.Error(err)) + return false, errs.ErrServerError + } + + return true, nil } +// XXX: untested func (p *profileServiceImpl) UpdateProfileSettings(cinfo dto.ClientInfo, newProfileSettings dto.ProfileSettingsDto) (bool, error) { - panic("unimplemented") + helper, db, err := database.NewDbHelperTransaction(p.dbctx); if err != nil { + p.log.Error( + "Failed to open transaction", + zap.Error(err)) + return false, err + } + defer helper.Rollback() + + // I wanted an automapper here but I'm feeling lazy, it's not used anywhere else regardless. + // Also this was initially meant to be a PATCH request but I realized that the fields in the + // DTO model are not pointers. Too late, guess this is a PUT request now. Who the hell cares + // about a couple extra bytes you have to send every now and then anyways. + err = db.TXlessQueries.UpdateProfileSettingsByUsername(db.CTX, database.UpdateProfileSettingsByUsernameParams{ + Username: cinfo.Username, + HideFulfilled: newProfileSettings.HideFulfilled, + HideProfileDetails: newProfileSettings.HideProfileDetails, + HideForUnauthenticated: newProfileSettings.HideForUnauthenticated, + HideBirthday: newProfileSettings.HideBirthday, + Captcha: newProfileSettings.Captcha, + FollowersOnlyInteraction: newProfileSettings.FollowersOnlyInteraction, + }); if err != nil { + p.log.Error( + "Failed to update user profile settings", + zap.String("username", cinfo.Username), + zap.Error(err)) + return false, errs.ErrServerError + } + + err = helper.Commit(); if err != nil { + p.log.Error( + "Failed to commit transaction", + zap.Error(err)) + return false, errs.ErrServerError + } + + return true, nil } +// TODO: implement S3 before I can do anything with it func (p *profileServiceImpl) UploadAvatar(cinfo dto.ClientInfo, filePath string) (*string, error) { panic("unimplemented") } diff --git a/sqlc/query.sql b/sqlc/query.sql index 81c0d83..ffdaa49 100644 --- a/sqlc/query.sql +++ b/sqlc/query.sql @@ -346,17 +346,19 @@ LIMIT 20 OFFSET 20 * $1; INSERT INTO profile_settings(profile_id) VALUES ($1) RETURNING *; -;-- name: UpdateProfileSettings :exec -UPDATE profile_settings +;-- name: UpdateProfileSettingsByUsername :exec +UPDATE profile_settings ps SET - hide_fulfilled = COALESCE($2, hide_fulfilled), - hide_profile_details = COALESCE($3, hide_profile_details), - hide_for_unauthenticated = COALESCE($4, hide_for_unauthenticated), - hide_birthday = COALESCE($5, hide_birthday), - hide_dates = COALESCE($6, hide_dates), - captcha = COALESCE($7, captcha), - followers_only_interaction = COALESCE($8, followers_only_interaction) -WHERE id = $1; + hide_fulfilled = COALESCE($2, ps.hide_fulfilled), + hide_profile_details = COALESCE($3, ps.hide_profile_details), + hide_for_unauthenticated = COALESCE($4, ps.hide_for_unauthenticated), + hide_birthday = COALESCE($5, ps.hide_birthday), + hide_dates = COALESCE($6, ps.hide_dates), + captcha = COALESCE($7, ps.captcha), + followers_only_interaction = COALESCE($8, ps.followers_only_interaction) +FROM profiles p +JOIN users u ON p.user_id = u.id +WHERE ps.profile_id = p.id AND u.username = $1; ;-- name: GetProfileSettingsByUsername :one SELECT profile_settings.* FROM profile_settings