155 lines
4.8 KiB
Go
155 lines
4.8 KiB
Go
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
|
|
}
|
|
}
|