slack5-1770544098/services/preferences-api/internal/api/handlers/preferences.go
rdev-worker a31f57382b
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature user-preferences
2026-02-08 10:47:23 +00:00

126 lines
3.4 KiB
Go

package handlers
import (
"errors"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"git.threesix.ai/jordan/slack5-1770544098/pkg/app"
"git.threesix.ai/jordan/slack5-1770544098/pkg/auth"
"git.threesix.ai/jordan/slack5-1770544098/pkg/httperror"
"git.threesix.ai/jordan/slack5-1770544098/pkg/httpresponse"
"git.threesix.ai/jordan/slack5-1770544098/pkg/logging"
"git.threesix.ai/jordan/slack5-1770544098/services/preferences-api/internal/domain"
"git.threesix.ai/jordan/slack5-1770544098/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"),
}
}
// UpdatePreferencesRequest is the request body for updating preferences.
type UpdatePreferencesRequest struct {
Preferences map[string]any `json:"preferences" validate:"required"`
}
// PreferencesResponse is the response DTO for user preferences.
type PreferencesResponse struct {
UserID string `json:"user_id"`
Preferences map[string]any `json:"preferences"`
UpdatedAt *time.Time `json:"updated_at"`
}
// Get returns preferences for a user.
func (h *Preferences) Get(w http.ResponseWriter, r *http.Request) error {
userID := chi.URLParam(r, "user_id")
if _, err := uuid.Parse(userID); err != nil {
return httperror.BadRequest("invalid user ID format")
}
if err := h.checkOwnership(r, userID); err != nil {
return err
}
prefs, err := h.svc.Get(r.Context(), userID)
if err != nil {
return err
}
httpresponse.OK(w, r, toPreferencesResponse(prefs))
return nil
}
// Update creates or updates preferences for a user.
func (h *Preferences) Update(w http.ResponseWriter, r *http.Request) error {
userID := chi.URLParam(r, "user_id")
if _, err := uuid.Parse(userID); err != nil {
return httperror.BadRequest("invalid user ID format")
}
var req UpdatePreferencesRequest
if err := app.BindAndValidate(r, &req); err != nil {
return err
}
if err := h.checkOwnership(r, userID); err != nil {
return err
}
prefs, err := h.svc.Update(r.Context(), userID, req.Preferences)
if err != nil {
return mapPreferencesDomainError(err)
}
httpresponse.OK(w, r, toPreferencesResponse(prefs))
return nil
}
// checkOwnership verifies the authenticated user owns the requested resource.
func (h *Preferences) checkOwnership(r *http.Request, userID string) error {
user := auth.MustGetUser(r.Context())
if user.ID != userID {
return httperror.Forbidden("access denied")
}
return nil
}
// toPreferencesResponse converts a domain UserPreferences to an API response.
func toPreferencesResponse(p *domain.UserPreferences) PreferencesResponse {
resp := PreferencesResponse{
UserID: p.UserID,
Preferences: p.Preferences,
}
if !p.UpdatedAt.IsZero() {
t := p.UpdatedAt
resp.UpdatedAt = &t
}
return resp
}
// mapPreferencesDomainError converts domain errors to HTTP errors.
func mapPreferencesDomainError(err error) error {
switch {
case errors.Is(err, domain.ErrInvalidPreferenceKey):
return httperror.BadRequest(err.Error())
case errors.Is(err, domain.ErrInvalidPreferenceValue):
return httperror.BadRequest(err.Error())
default:
return err
}
}