package handlers import ( "errors" "net/http" "time" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/slack5-1770574304/pkg/app" "git.threesix.ai/jordan/slack5-1770574304/pkg/auth" "git.threesix.ai/jordan/slack5-1770574304/pkg/httperror" "git.threesix.ai/jordan/slack5-1770574304/pkg/httpresponse" "git.threesix.ai/jordan/slack5-1770574304/pkg/logging" "git.threesix.ai/jordan/slack5-1770574304/services/preferences-api/internal/domain" "git.threesix.ai/jordan/slack5-1770574304/services/preferences-api/internal/service" ) // Preference handles HTTP requests for user preference resources. type Preference struct { svc *service.PreferenceService logger *logging.Logger } // NewPreference creates a new Preference handler with injected dependencies. func NewPreference(svc *service.PreferenceService, logger *logging.Logger) *Preference { return &Preference{ svc: svc, logger: logger.WithComponent("PreferenceHandler"), } } // UpdatePreferencesRequest is the request body for updating preferences. type UpdatePreferencesRequest struct { Theme string `json:"theme" validate:"required,oneof=light dark system"` Language string `json:"language" validate:"required,oneof=en fr es de ja"` Notifications UpdateNotificationsRequest `json:"notifications" validate:"required"` } // UpdateNotificationsRequest is the nested notification preferences in the request. type UpdateNotificationsRequest struct { Email bool `json:"email"` Push bool `json:"push"` Digest string `json:"digest" validate:"required,oneof=none daily weekly"` } // PreferencesResponse is the API response for user preferences. type PreferencesResponse struct { UserID string `json:"user_id"` Theme string `json:"theme"` Language string `json:"language"` Notifications NotificationsResponse `json:"notifications"` UpdatedAt string `json:"updated_at,omitempty"` } // NotificationsResponse is the nested notification preferences in the response. type NotificationsResponse struct { Email bool `json:"email"` Push bool `json:"push"` Digest string `json:"digest"` } // toPreferencesResponse converts domain preferences to an API response. func toPreferencesResponse(p *domain.UserPreferences) PreferencesResponse { resp := PreferencesResponse{ UserID: p.UserID, Theme: string(p.Theme), Language: p.Language, Notifications: NotificationsResponse{ Email: p.Notifications.Email, Push: p.Notifications.Push, Digest: string(p.Notifications.Digest), }, } if !p.UpdatedAt.IsZero() { resp.UpdatedAt = p.UpdatedAt.Format(time.RFC3339) } return resp } // Get returns preferences for a user. func (h *Preference) Get(w http.ResponseWriter, r *http.Request) error { userID := chi.URLParam(r, "user_id") user := auth.GetUser(r.Context()) if user == nil { return httperror.Unauthorized("authentication required") } // Authorization: self-access or admin read if user.ID != userID && !user.HasRole("admin") { return httperror.Forbidden("access denied: cannot access another user's preferences") } prefs, err := h.svc.GetPreferences(r.Context(), userID) if err != nil { return err } httpresponse.OK(w, r, toPreferencesResponse(prefs)) return nil } // Update creates or replaces preferences for a user. func (h *Preference) Update(w http.ResponseWriter, r *http.Request) error { userID := chi.URLParam(r, "user_id") user := auth.GetUser(r.Context()) if user == nil { return httperror.Unauthorized("authentication required") } // Authorization: self-access only (even admins cannot write other users' preferences) if user.ID != userID { return httperror.Forbidden("access denied: cannot modify another user's preferences") } var req UpdatePreferencesRequest if err := app.BindAndValidateStrict(r, &req); err != nil { return err } prefs := &domain.UserPreferences{ Theme: domain.Theme(req.Theme), Language: req.Language, Notifications: domain.NotificationPreferences{ Email: req.Notifications.Email, Push: req.Notifications.Push, Digest: domain.DigestFrequency(req.Notifications.Digest), }, } result, err := h.svc.UpdatePreferences(r.Context(), userID, prefs) if err != nil { return mapPreferenceDomainError(err) } httpresponse.OK(w, r, toPreferencesResponse(result)) return nil } // mapPreferenceDomainError converts domain errors to HTTP errors. func mapPreferenceDomainError(err error) error { switch { case errors.Is(err, domain.ErrInvalidTheme): return httperror.BadRequest(domain.ErrInvalidTheme.Error()) case errors.Is(err, domain.ErrInvalidLanguage): return httperror.BadRequest(domain.ErrInvalidLanguage.Error()) case errors.Is(err, domain.ErrInvalidDigest): return httperror.BadRequest(domain.ErrInvalidDigest.Error()) default: return err } }