diff --git a/internal/service/play_profile.go b/internal/service/play_profile.go index b32aed6..a85be7c 100644 --- a/internal/service/play_profile.go +++ b/internal/service/play_profile.go @@ -10,8 +10,6 @@ package service import ( "context" - "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -45,42 +43,6 @@ func NewPlayProfileService(log *zap.Logger, repo *repository.Container) *PlayPro return &PlayProfileService{log: log, repo: repo} } -// PlayProfileInput is the create/update payload accepted by the API. -// PIN is hashed only when non-empty so omitting it preserves the -// existing PIN on update. -type PlayProfileInput struct { - UserID string `json:"user_id"` - Name string `json:"name"` - IsDefault bool `json:"is_default"` - ContentRatingLimit string `json:"content_rating_limit"` - AllowAdult bool `json:"allow_adult"` - RequirePIN bool `json:"require_pin"` - PIN string `json:"pin,omitempty"` - PreferredSubtitleLang string `json:"preferred_subtitle_lang"` - PreferredAudioLang string `json:"preferred_audio_lang"` - AutoplayNext bool `json:"autoplay_next"` - SkipIntro bool `json:"skip_intro"` - AllowedLibraryIDs []string `json:"allowed_library_ids"` -} - -// ProfileView is the public shape — AllowedLibraryIDs is decoded back -// to a slice so the React form can bind to it directly. -type ProfileView struct { - model.PlayProfile - AllowedLibraryIDs []string `json:"allowed_library_ids"` -} - -func toProfileView(p model.PlayProfile) ProfileView { - v := ProfileView{PlayProfile: p} - if p.AllowedLibraryIDs != "" { - _ = json.Unmarshal([]byte(p.AllowedLibraryIDs), &v.AllowedLibraryIDs) - } - if v.AllowedLibraryIDs == nil { - v.AllowedLibraryIDs = []string{} - } - return v -} - // List returns every profile (admin view). func (s *PlayProfileService) List(ctx context.Context) ([]ProfileView, error) { rows, err := s.repo.PlayProfile.List(ctx) @@ -266,32 +228,3 @@ func (s *PlayProfileService) TouchActive(ctx context.Context, id string) error { "last_active_at": &now, }) } - -// validateProfileInput rejects malformed payloads. On create we require -// user_id; on update we allow it to be empty (caller supplies it via URL). -// Errors wrap ErrPlayProfileValidation so handlers can distinguish -// validation failures (400) from infrastructure errors (500). -func validateProfileInput(in PlayProfileInput, requireUser bool) error { - if strings.TrimSpace(in.Name) == "" { - return fmt.Errorf("%w: name required", ErrPlayProfileValidation) - } - if requireUser && strings.TrimSpace(in.UserID) == "" { - return fmt.Errorf("%w: user_id required", ErrPlayProfileValidation) - } - if requireUser && in.RequirePIN && strings.TrimSpace(in.PIN) == "" { - return fmt.Errorf("%w: pin required", ErrPlayProfileValidation) - } - if in.RequirePIN && in.PIN != "" { - if len(in.PIN) < 4 || len(in.PIN) > 8 { - return fmt.Errorf("%w: pin must be 4-8 characters", ErrPlayProfileValidation) - } - } - return nil -} - -// hashPIN is a deterministic SHA-256 (PINs are short numeric secrets; -// bcrypt is overkill for this use case). -func hashPIN(pin string) string { - sum := sha256.Sum256([]byte(pin)) - return hex.EncodeToString(sum[:]) -} diff --git a/internal/service/play_profile_model.go b/internal/service/play_profile_model.go new file mode 100644 index 0000000..c600b5a --- /dev/null +++ b/internal/service/play_profile_model.go @@ -0,0 +1,68 @@ +package service + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/ShukeBta/MediaStationGo/internal/model" +) + +// PlayProfileInput is the create/update payload accepted by the API. +// PIN is hashed only when non-empty so omitting it preserves the existing PIN on update. +type PlayProfileInput struct { + UserID string `json:"user_id"` + Name string `json:"name"` + IsDefault bool `json:"is_default"` + ContentRatingLimit string `json:"content_rating_limit"` + AllowAdult bool `json:"allow_adult"` + RequirePIN bool `json:"require_pin"` + PIN string `json:"pin,omitempty"` + PreferredSubtitleLang string `json:"preferred_subtitle_lang"` + PreferredAudioLang string `json:"preferred_audio_lang"` + AutoplayNext bool `json:"autoplay_next"` + SkipIntro bool `json:"skip_intro"` + AllowedLibraryIDs []string `json:"allowed_library_ids"` +} + +// ProfileView is the public shape for React forms. +type ProfileView struct { + model.PlayProfile + AllowedLibraryIDs []string `json:"allowed_library_ids"` +} + +func toProfileView(p model.PlayProfile) ProfileView { + v := ProfileView{PlayProfile: p} + if p.AllowedLibraryIDs != "" { + _ = json.Unmarshal([]byte(p.AllowedLibraryIDs), &v.AllowedLibraryIDs) + } + if v.AllowedLibraryIDs == nil { + v.AllowedLibraryIDs = []string{} + } + return v +} + +func validateProfileInput(in PlayProfileInput, requireUser bool) error { + if strings.TrimSpace(in.Name) == "" { + return fmt.Errorf("%w: name required", ErrPlayProfileValidation) + } + if requireUser && strings.TrimSpace(in.UserID) == "" { + return fmt.Errorf("%w: user_id required", ErrPlayProfileValidation) + } + if requireUser && in.RequirePIN && strings.TrimSpace(in.PIN) == "" { + return fmt.Errorf("%w: pin required", ErrPlayProfileValidation) + } + if in.RequirePIN && in.PIN != "" { + if len(in.PIN) < 4 || len(in.PIN) > 8 { + return fmt.Errorf("%w: pin must be 4-8 characters", ErrPlayProfileValidation) + } + } + return nil +} + +func hashPIN(pin string) string { + sum := sha256.Sum256([]byte(pin)) + return hex.EncodeToString(sum[:]) +}