feat(users): add user_preferences migration with appearance fields

This commit is contained in:
senke 2026-02-25 09:45:03 +01:00
parent 93666a3390
commit 6f4c9c50ff
4 changed files with 83 additions and 24 deletions

View file

@ -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, &notificationsJSON, &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)
}

View file

@ -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).

View file

@ -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"`
}

View file

@ -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);