feat(users): add user_preferences migration with appearance fields
This commit is contained in:
parent
93666a3390
commit
6f4c9c50ff
4 changed files with 83 additions and 24 deletions
|
|
@ -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, ¬ificationsJSON, &privacyJSON,
|
&preferences.Timezone, ¬ificationsJSON, &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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
veza-backend-api/migrations/118_user_preferences.sql
Normal file
17
veza-backend-api/migrations/118_user_preferences.sql
Normal 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);
|
||||||
Loading…
Reference in a new issue