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 ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -398,6 +399,10 @@ func (s *Service) GetUserPreferences(userID uuid.UUID) (*UserPreferencesResponse
COALESCE(notifications, '{}') as notifications, COALESCE(notifications, '{}') as notifications,
COALESCE(privacy, '{}') as privacy, COALESCE(privacy, '{}') as privacy,
COALESCE(audio, '{}') as audio, 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 updated_at
FROM user_preferences FROM user_preferences
WHERE user_id = $1 WHERE user_id = $1
@ -409,7 +414,8 @@ func (s *Service) GetUserPreferences(userID uuid.UUID) (*UserPreferencesResponse
err := s.db.QueryRow(query, userID).Scan( err := s.db.QueryRow(query, userID).Scan(
&preferences.UserID, &preferences.Theme, &preferences.Language, &preferences.UserID, &preferences.Theme, &preferences.Language,
&preferences.Timezone, &notificationsJSON, &privacyJSON, &preferences.Timezone, &notificationsJSON, &privacyJSON,
&audioJSON, &preferences.UpdatedAt, &audioJSON, &preferences.Contrast, &preferences.Density,
&preferences.AccentHue, &preferences.FontSize, &preferences.UpdatedAt,
) )
if err != nil { if err != nil {
@ -432,24 +438,34 @@ func (s *Service) GetUserPreferences(userID uuid.UUID) (*UserPreferencesResponse
Audio: AudioSettings{ Audio: AudioSettings{
AutoPlay: true, Quality: "high", Volume: 0.8, Crossfade: 5, AutoPlay: true, Quality: "high", Volume: 0.8, Crossfade: 5,
}, },
Contrast: "normal",
Density: "comfortable",
AccentHue: 220,
FontSize: 16,
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, nil }, nil
} }
return nil, fmt.Errorf("failed to get user preferences: %w", err) return nil, fmt.Errorf("failed to get user preferences: %w", err)
} }
// TODO: Parse JSON strings to structs (simplified for now) // Parse JSON strings to structs
preferences.Notifications = NotificationSettings{ if err := json.Unmarshal([]byte(notificationsJSON), &preferences.Notifications); err != nil {
Email: true, Push: true, Desktop: true, preferences.Notifications = NotificationSettings{
NewFollowers: true, TrackComments: true, Email: true, Push: true, Desktop: true,
DirectMessages: true, Mentions: true, Likes: false, NewFollowers: true, TrackComments: true,
DirectMessages: true, Mentions: true, Likes: false,
}
} }
preferences.Privacy = PrivacySettings{ if err := json.Unmarshal([]byte(privacyJSON), &preferences.Privacy); err != nil {
ShowEmail: false, ShowActivity: true, AllowDM: true, preferences.Privacy = PrivacySettings{
TrackVisibility: "public", ProfileVisibility: "public", ShowEmail: false, ShowActivity: true, AllowDM: true,
TrackVisibility: "public", ProfileVisibility: "public",
}
} }
preferences.Audio = AudioSettings{ if err := json.Unmarshal([]byte(audioJSON), &preferences.Audio); err != nil {
AutoPlay: true, Quality: "high", Volume: 0.8, Crossfade: 5, preferences.Audio = AudioSettings{
AutoPlay: true, Quality: "high", Volume: 0.8, Crossfade: 5,
}
} }
return &preferences, nil return &preferences, nil
@ -482,13 +498,30 @@ func (s *Service) UpdateUserPreferences(userID uuid.UUID, req UserPreferencesReq
if req.Audio != nil { if req.Audio != nil {
current.Audio = *req.Audio 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() 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) // Sauvegarder en base (upsert)
query := ` query := `
INSERT INTO user_preferences (user_id, theme, language, timezone, notifications, privacy, audio, updated_at) 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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (user_id) DO UPDATE SET ON CONFLICT (user_id) DO UPDATE SET
theme = EXCLUDED.theme, theme = EXCLUDED.theme,
language = EXCLUDED.language, language = EXCLUDED.language,
@ -496,16 +529,17 @@ func (s *Service) UpdateUserPreferences(userID uuid.UUID, req UserPreferencesReq
notifications = EXCLUDED.notifications, notifications = EXCLUDED.notifications,
privacy = EXCLUDED.privacy, privacy = EXCLUDED.privacy,
audio = EXCLUDED.audio, audio = EXCLUDED.audio,
contrast = EXCLUDED.contrast,
density = EXCLUDED.density,
accent_hue = EXCLUDED.accent_hue,
font_size = EXCLUDED.font_size,
updated_at = EXCLUDED.updated_at 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, _, 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 { if err != nil {
return nil, fmt.Errorf("failed to update user preferences: %w", err) 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() userID := uuid.New()
// Expect GetUserPreferences first // Expect GetUserPreferences first
prefRows := sqlmock.NewRows([]string{"user_id", "theme", "language", "timezone", "notifications", "privacy", "audio", "updated_at"}). 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", "{}", "{}", "{}", time.Now()) AddRow(userID, "light", "en", "UTC", "{}", "{}", "{}", "normal", "comfortable", 220, 16, time.Now())
mock.ExpectQuery(regexp.QuoteMeta(`SELECT user_id`)). mock.ExpectQuery(regexp.QuoteMeta(`SELECT user_id`)).
WithArgs(userID). WithArgs(userID).
@ -263,7 +263,7 @@ func TestService_UpdateUserPreferences_Success(t *testing.T) {
} }
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO user_preferences`)). 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)) WillReturnResult(sqlmock.NewResult(1, 1))
pref, err := service.UpdateUserPreferences(userID, req) pref, err := service.UpdateUserPreferences(userID, req)
@ -373,8 +373,8 @@ func TestService_ExportUserData_Success(t *testing.T) {
WillReturnRows(userRows) WillReturnRows(userRows)
// 2. GetUserPreferences // 2. GetUserPreferences
prefRows := sqlmock.NewRows([]string{"user_id", "theme", "language", "timezone", "notifications", "privacy", "audio", "updated_at"}). 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", "{}", "{}", "{}", time.Now()) AddRow(userID, "light", "en", "UTC", "{}", "{}", "{}", "normal", "comfortable", 220, 16, time.Now())
mock.ExpectQuery(regexp.QuoteMeta(`SELECT user_id`)). mock.ExpectQuery(regexp.QuoteMeta(`SELECT user_id`)).
WithArgs(userID). WithArgs(userID).

View file

@ -73,6 +73,10 @@ type UserPreferencesRequest struct {
Notifications *NotificationSettings `json:"notifications,omitempty"` Notifications *NotificationSettings `json:"notifications,omitempty"`
Privacy *PrivacySettings `json:"privacy,omitempty"` Privacy *PrivacySettings `json:"privacy,omitempty"`
Audio *AudioSettings `json:"audio,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 // UserPreferencesResponse représente les préférences utilisateur
@ -84,6 +88,10 @@ type UserPreferencesResponse struct {
Notifications NotificationSettings `json:"notifications"` Notifications NotificationSettings `json:"notifications"`
Privacy PrivacySettings `json:"privacy"` Privacy PrivacySettings `json:"privacy"`
Audio AudioSettings `json:"audio"` 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"` 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);