slack5-1770541397/services/preferences-api/internal/api/handlers/preferences.go
rdev-worker e3e19a3fa8
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /implement-feature user-preferences
2026-02-08 09:29:22 +00:00

145 lines
4.6 KiB
Go

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
}
}