package handlers import ( "errors" "net/http" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/slack5-1770541397/pkg/app" "git.threesix.ai/jordan/slack5-1770541397/pkg/auth" "git.threesix.ai/jordan/slack5-1770541397/pkg/httperror" "git.threesix.ai/jordan/slack5-1770541397/pkg/httpresponse" "git.threesix.ai/jordan/slack5-1770541397/pkg/logging" "git.threesix.ai/jordan/slack5-1770541397/services/preferences-api/internal/domain" "git.threesix.ai/jordan/slack5-1770541397/services/preferences-api/internal/service" ) // Preferences handles HTTP requests for user preferences. type Preferences struct { svc *service.PreferencesService logger *logging.Logger } // NewPreferences creates a new Preferences handler with injected dependencies. func NewPreferences(svc *service.PreferencesService, logger *logging.Logger) *Preferences { return &Preferences{ svc: svc, logger: logger.WithComponent("PreferencesHandler"), } } // PutPreferencesRequest is the request body for setting preferences. type PutPreferencesRequest struct { Preferences PreferencesPayload `json:"preferences" validate:"required"` } // PreferencesPayload represents the preferences JSON structure in requests/responses. type PreferencesPayload struct { Theme string `json:"theme,omitempty"` Language string `json:"language,omitempty"` Notifications *NotificationPreferencesPayload `json:"notifications,omitempty"` } // NotificationPreferencesPayload represents notification settings in requests/responses. type NotificationPreferencesPayload struct { Email bool `json:"email"` Push bool `json:"push"` SMS bool `json:"sms"` } // PreferencesResponse is the response for a user's preferences. type PreferencesResponse struct { UserID string `json:"user_id"` Preferences PreferencesPayload `json:"preferences"` UpdatedAt string `json:"updated_at"` } // toPreferencesResponse converts a domain UserPreferences to an API response. func toPreferencesResponse(up *domain.UserPreferences) PreferencesResponse { return PreferencesResponse{ UserID: up.UserID.String(), Preferences: PreferencesPayload{ Theme: up.Preferences.Theme, Language: up.Preferences.Language, Notifications: &NotificationPreferencesPayload{ Email: up.Preferences.Notifications.Email, Push: up.Preferences.Notifications.Push, SMS: up.Preferences.Notifications.SMS, }, }, UpdatedAt: up.UpdatedAt.Format("2006-01-02T15:04:05Z"), } } // toDomainPreferences converts an API payload to a domain Preferences. func toDomainPreferences(p PreferencesPayload) domain.Preferences { prefs := domain.Preferences{ Theme: p.Theme, Language: p.Language, } if p.Notifications != nil { prefs.Notifications = domain.NotificationPreferences{ Email: p.Notifications.Email, Push: p.Notifications.Push, SMS: p.Notifications.SMS, } } return prefs } // Get returns the preferences for a user. func (h *Preferences) Get(w http.ResponseWriter, r *http.Request) error { userID := chi.URLParam(r, "user_id") // Authorization: authenticated user must match path user_id authUser := auth.GetUser(r.Context()) if authUser == nil || authUser.ID != userID { return httperror.Forbidden("access denied: can only access own preferences") } prefs, err := h.svc.GetPreferences(r.Context(), domain.UserID(userID)) if err != nil { return mapPreferencesDomainError(err) } httpresponse.OK(w, r, toPreferencesResponse(prefs)) return nil } // Put creates or replaces preferences for a user. func (h *Preferences) Put(w http.ResponseWriter, r *http.Request) error { userID := chi.URLParam(r, "user_id") // Authorization: authenticated user must match path user_id authUser := auth.GetUser(r.Context()) if authUser == nil || authUser.ID != userID { return httperror.Forbidden("access denied: can only modify own preferences") } var req PutPreferencesRequest if err := app.BindAndValidate(r, &req); err != nil { return err } prefs, err := h.svc.SetPreferences(r.Context(), domain.UserID(userID), toDomainPreferences(req.Preferences)) if err != nil { return mapPreferencesDomainError(err) } httpresponse.OK(w, r, toPreferencesResponse(prefs)) return nil } // mapPreferencesDomainError converts domain errors to HTTP errors. func mapPreferencesDomainError(err error) error { switch { case errors.Is(err, domain.ErrInvalidTheme): return httperror.BadRequest("invalid theme: must be one of light, dark, system") case errors.Is(err, domain.ErrInvalidLanguage): return httperror.BadRequest("invalid language: must be at most 10 characters") default: return err } }