From 6f4c9c50ff1715b6957816c94abaefb4e89e7202 Mon Sep 17 00:00:00 2001 From: senke Date: Wed, 25 Feb 2026 09:45:03 +0100 Subject: [PATCH] feat(users): add user_preferences migration with appearance fields --- veza-backend-api/internal/api/user/service.go | 72 ++++++++++++++----- .../internal/api/user/service_test.go | 10 +-- veza-backend-api/internal/api/user/types.go | 8 +++ .../migrations/118_user_preferences.sql | 17 +++++ 4 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 veza-backend-api/migrations/118_user_preferences.sql diff --git a/veza-backend-api/internal/api/user/service.go b/veza-backend-api/internal/api/user/service.go index 674c61561..9c2f76bfc 100644 --- a/veza-backend-api/internal/api/user/service.go +++ b/veza-backend-api/internal/api/user/service.go @@ -3,6 +3,7 @@ package user import ( "database/sql" + "encoding/json" "fmt" "strings" "time" @@ -398,6 +399,10 @@ func (s *Service) GetUserPreferences(userID uuid.UUID) (*UserPreferencesResponse COALESCE(notifications, '{}') as notifications, COALESCE(privacy, '{}') as privacy, COALESCE(audio, '{}') as audio, + COALESCE(contrast, 'normal') as contrast, + COALESCE(density, 'comfortable') as density, + COALESCE(accent_hue, 220) as accent_hue, + COALESCE(font_size, 16) as font_size, updated_at FROM user_preferences WHERE user_id = $1 @@ -409,7 +414,8 @@ func (s *Service) GetUserPreferences(userID uuid.UUID) (*UserPreferencesResponse err := s.db.QueryRow(query, userID).Scan( &preferences.UserID, &preferences.Theme, &preferences.Language, &preferences.Timezone, ¬ificationsJSON, &privacyJSON, - &audioJSON, &preferences.UpdatedAt, + &audioJSON, &preferences.Contrast, &preferences.Density, + &preferences.AccentHue, &preferences.FontSize, &preferences.UpdatedAt, ) if err != nil { @@ -432,24 +438,34 @@ func (s *Service) GetUserPreferences(userID uuid.UUID) (*UserPreferencesResponse Audio: AudioSettings{ AutoPlay: true, Quality: "high", Volume: 0.8, Crossfade: 5, }, + Contrast: "normal", + Density: "comfortable", + AccentHue: 220, + FontSize: 16, UpdatedAt: time.Now(), }, nil } return nil, fmt.Errorf("failed to get user preferences: %w", err) } - // TODO: Parse JSON strings to structs (simplified for now) - preferences.Notifications = NotificationSettings{ - Email: true, Push: true, Desktop: true, - NewFollowers: true, TrackComments: true, - DirectMessages: true, Mentions: true, Likes: false, + // Parse JSON strings to structs + if err := json.Unmarshal([]byte(notificationsJSON), &preferences.Notifications); err != nil { + preferences.Notifications = NotificationSettings{ + Email: true, Push: true, Desktop: true, + NewFollowers: true, TrackComments: true, + DirectMessages: true, Mentions: true, Likes: false, + } } - preferences.Privacy = PrivacySettings{ - ShowEmail: false, ShowActivity: true, AllowDM: true, - TrackVisibility: "public", ProfileVisibility: "public", + if err := json.Unmarshal([]byte(privacyJSON), &preferences.Privacy); err != nil { + preferences.Privacy = PrivacySettings{ + ShowEmail: false, ShowActivity: true, AllowDM: true, + TrackVisibility: "public", ProfileVisibility: "public", + } } - preferences.Audio = AudioSettings{ - AutoPlay: true, Quality: "high", Volume: 0.8, Crossfade: 5, + if err := json.Unmarshal([]byte(audioJSON), &preferences.Audio); err != nil { + preferences.Audio = AudioSettings{ + AutoPlay: true, Quality: "high", Volume: 0.8, Crossfade: 5, + } } return &preferences, nil @@ -482,13 +498,30 @@ func (s *Service) UpdateUserPreferences(userID uuid.UUID, req UserPreferencesReq if req.Audio != nil { current.Audio = *req.Audio } + if req.Contrast != nil { + current.Contrast = *req.Contrast + } + if req.Density != nil { + current.Density = *req.Density + } + if req.AccentHue != nil { + current.AccentHue = *req.AccentHue + } + if req.FontSize != nil { + current.FontSize = *req.FontSize + } current.UpdatedAt = time.Now() + // Serialize structs to JSON + notificationsJSON, _ := json.Marshal(current.Notifications) + privacyJSON, _ := json.Marshal(current.Privacy) + audioJSON, _ := json.Marshal(current.Audio) + // Sauvegarder en base (upsert) query := ` - INSERT INTO user_preferences (user_id, theme, language, timezone, notifications, privacy, audio, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + INSERT INTO user_preferences (user_id, theme, language, timezone, notifications, privacy, audio, contrast, density, accent_hue, font_size, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (user_id) DO UPDATE SET theme = EXCLUDED.theme, language = EXCLUDED.language, @@ -496,16 +529,17 @@ func (s *Service) UpdateUserPreferences(userID uuid.UUID, req UserPreferencesReq notifications = EXCLUDED.notifications, privacy = EXCLUDED.privacy, audio = EXCLUDED.audio, + contrast = EXCLUDED.contrast, + density = EXCLUDED.density, + accent_hue = EXCLUDED.accent_hue, + font_size = EXCLUDED.font_size, updated_at = EXCLUDED.updated_at ` - // TODO: Serialize structs to JSON (simplified for now) - notificationsJSON := "{}" - privacyJSON := "{}" - audioJSON := "{}" - _, err = s.db.Exec(query, userID, current.Theme, current.Language, current.Timezone, - notificationsJSON, privacyJSON, audioJSON, current.UpdatedAt) + string(notificationsJSON), string(privacyJSON), string(audioJSON), + current.Contrast, current.Density, current.AccentHue, current.FontSize, + current.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to update user preferences: %w", err) } diff --git a/veza-backend-api/internal/api/user/service_test.go b/veza-backend-api/internal/api/user/service_test.go index fe0a305bc..cb4c997bb 100644 --- a/veza-backend-api/internal/api/user/service_test.go +++ b/veza-backend-api/internal/api/user/service_test.go @@ -249,8 +249,8 @@ func TestService_UpdateUserPreferences_Success(t *testing.T) { userID := uuid.New() // Expect GetUserPreferences first - prefRows := sqlmock.NewRows([]string{"user_id", "theme", "language", "timezone", "notifications", "privacy", "audio", "updated_at"}). - AddRow(userID, "light", "en", "UTC", "{}", "{}", "{}", time.Now()) + prefRows := sqlmock.NewRows([]string{"user_id", "theme", "language", "timezone", "notifications", "privacy", "audio", "contrast", "density", "accent_hue", "font_size", "updated_at"}). + AddRow(userID, "light", "en", "UTC", "{}", "{}", "{}", "normal", "comfortable", 220, 16, time.Now()) mock.ExpectQuery(regexp.QuoteMeta(`SELECT user_id`)). WithArgs(userID). @@ -263,7 +263,7 @@ func TestService_UpdateUserPreferences_Success(t *testing.T) { } mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO user_preferences`)). - WithArgs(userID, "dark", "en", "UTC", "{}", "{}", "{}", sqlmock.AnyArg()). + WithArgs(userID, "dark", "en", "UTC", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "normal", "comfortable", 220, 16, sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(1, 1)) pref, err := service.UpdateUserPreferences(userID, req) @@ -373,8 +373,8 @@ func TestService_ExportUserData_Success(t *testing.T) { WillReturnRows(userRows) // 2. GetUserPreferences - prefRows := sqlmock.NewRows([]string{"user_id", "theme", "language", "timezone", "notifications", "privacy", "audio", "updated_at"}). - AddRow(userID, "light", "en", "UTC", "{}", "{}", "{}", time.Now()) + prefRows := sqlmock.NewRows([]string{"user_id", "theme", "language", "timezone", "notifications", "privacy", "audio", "contrast", "density", "accent_hue", "font_size", "updated_at"}). + AddRow(userID, "light", "en", "UTC", "{}", "{}", "{}", "normal", "comfortable", 220, 16, time.Now()) mock.ExpectQuery(regexp.QuoteMeta(`SELECT user_id`)). WithArgs(userID). diff --git a/veza-backend-api/internal/api/user/types.go b/veza-backend-api/internal/api/user/types.go index d838f7a04..393226f06 100644 --- a/veza-backend-api/internal/api/user/types.go +++ b/veza-backend-api/internal/api/user/types.go @@ -73,6 +73,10 @@ type UserPreferencesRequest struct { Notifications *NotificationSettings `json:"notifications,omitempty"` Privacy *PrivacySettings `json:"privacy,omitempty"` Audio *AudioSettings `json:"audio,omitempty"` + Contrast *string `json:"contrast,omitempty"` + Density *string `json:"density,omitempty"` + AccentHue *int `json:"accentHue,omitempty"` + FontSize *int `json:"fontSize,omitempty"` } // UserPreferencesResponse représente les préférences utilisateur @@ -84,6 +88,10 @@ type UserPreferencesResponse struct { Notifications NotificationSettings `json:"notifications"` Privacy PrivacySettings `json:"privacy"` Audio AudioSettings `json:"audio"` + Contrast string `json:"contrast"` + Density string `json:"density"` + AccentHue int `json:"accentHue"` + FontSize int `json:"fontSize"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/veza-backend-api/migrations/118_user_preferences.sql b/veza-backend-api/migrations/118_user_preferences.sql new file mode 100644 index 000000000..18aff2fef --- /dev/null +++ b/veza-backend-api/migrations/118_user_preferences.sql @@ -0,0 +1,17 @@ +-- v0.801: User preferences table with appearance fields (contrast, density, accent_hue, font_size) +CREATE TABLE IF NOT EXISTS user_preferences ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + theme VARCHAR(20) NOT NULL DEFAULT 'light', + language VARCHAR(10) NOT NULL DEFAULT 'en', + timezone VARCHAR(50) NOT NULL DEFAULT 'UTC', + notifications JSONB NOT NULL DEFAULT '{}', + privacy JSONB NOT NULL DEFAULT '{}', + audio JSONB NOT NULL DEFAULT '{}', + contrast VARCHAR(10) NOT NULL DEFAULT 'normal', + density VARCHAR(12) NOT NULL DEFAULT 'comfortable', + accent_hue INT NOT NULL DEFAULT 220, + font_size INT NOT NULL DEFAULT 16, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);