208 lines
7.2 KiB
Go
208 lines
7.2 KiB
Go
package site
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type Service struct {
|
|
repo Repository
|
|
}
|
|
|
|
func NewService(repo Repository) *Service {
|
|
return &Service{repo: repo}
|
|
}
|
|
|
|
func (s *Service) Get(ctx context.Context) (*Settings, error) {
|
|
return s.repo.Get(ctx)
|
|
}
|
|
|
|
func (s *Service) Update(ctx context.Context, name, description string, ordersEnabled bool) (*Settings, error) {
|
|
settings := &Settings{Name: name, Description: description, OrdersEnabled: ordersEnabled}
|
|
if err := s.repo.Update(ctx, settings); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.Get(ctx)
|
|
}
|
|
|
|
// AppearanceInput is updated independently from site identity (name/slug):
|
|
// the "Appearance" admin page only ever touches these fields, so it must
|
|
// not be blocked by identity validation (e.g. an unset slug) or clobber
|
|
// name/description/slug on save. Each field is a pointer so the admin can
|
|
// change a single option (e.g. just the product layout) without resending
|
|
// every color -- nil fields keep their current, already-saved value.
|
|
type AppearanceInput struct {
|
|
HeaderBgColor *string
|
|
HeaderTextColor *string
|
|
BodyBgColor *string
|
|
BodyTextColor *string
|
|
FooterBgColor *string
|
|
FooterTextColor *string
|
|
AccentColor *string
|
|
ProductLayout *string
|
|
ProductColumns *int
|
|
ProductScroll *string
|
|
|
|
ContactCardTransparent *bool
|
|
ContactCardBgColor *string
|
|
|
|
// LogoMediaIDSet/HeroMediaIDSet distinguish "the admin didn't touch
|
|
// this field" (false: keep the current value) from "the admin
|
|
// explicitly chose an image, possibly clearing it to nil" (true: use
|
|
// the ID as-is, nil included) -- same reasoning as
|
|
// CustomerAuthInput.VerificationContactLinkSet.
|
|
LogoMediaIDSet bool
|
|
LogoMediaID *uuid.UUID
|
|
HeroMediaIDSet bool
|
|
HeroMediaID *uuid.UUID
|
|
HeroPages *string
|
|
}
|
|
|
|
func (s *Service) UpdateAppearance(ctx context.Context, in AppearanceInput) (*Settings, error) {
|
|
current, err := s.repo.Get(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
settings := &Settings{
|
|
HeaderBgColor: applyIfSet(in.HeaderBgColor, current.HeaderBgColor),
|
|
HeaderTextColor: applyIfSet(in.HeaderTextColor, current.HeaderTextColor),
|
|
BodyBgColor: applyIfSet(in.BodyBgColor, current.BodyBgColor),
|
|
BodyTextColor: applyIfSet(in.BodyTextColor, current.BodyTextColor),
|
|
FooterBgColor: applyIfSet(in.FooterBgColor, current.FooterBgColor),
|
|
FooterTextColor: applyIfSet(in.FooterTextColor, current.FooterTextColor),
|
|
AccentColor: applyIfSet(in.AccentColor, current.AccentColor),
|
|
ProductLayout: applyIfSet(in.ProductLayout, current.ProductLayout),
|
|
ProductColumns: applyIntIfSet(in.ProductColumns, current.ProductColumns),
|
|
ProductScroll: applyIfSet(in.ProductScroll, current.ProductScroll),
|
|
ContactCardTransparent: applyBoolIfSet(in.ContactCardTransparent, current.ContactCardTransparent),
|
|
ContactCardBgColor: applyIfSet(in.ContactCardBgColor, current.ContactCardBgColor),
|
|
LogoMediaID: current.LogoMediaID,
|
|
HeroMediaID: current.HeroMediaID,
|
|
HeroPages: applyIfSet(in.HeroPages, current.HeroPages),
|
|
}
|
|
if in.LogoMediaIDSet {
|
|
settings.LogoMediaID = in.LogoMediaID
|
|
}
|
|
if in.HeroMediaIDSet {
|
|
settings.HeroMediaID = in.HeroMediaID
|
|
}
|
|
if err := s.repo.UpdateAppearance(ctx, settings); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.Get(ctx)
|
|
}
|
|
|
|
func applyIfSet(value *string, fallback string) string {
|
|
if value == nil {
|
|
return fallback
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func applyIntIfSet(value *int, fallback int) int {
|
|
if value == nil {
|
|
return fallback
|
|
}
|
|
return *value
|
|
}
|
|
|
|
// CustomerAuthInput is updated independently from identity/appearance, same
|
|
// reasoning as AppearanceInput: the "Customer accounts" admin page only
|
|
// ever touches these fields. LoginEnabled and RegistrationEnabled are two
|
|
// independent switches (e.g. an invite-only shop can enable login while
|
|
// keeping registration off), so each is its own optional pointer.
|
|
type CustomerAuthInput struct {
|
|
LoginEnabled *bool
|
|
RegistrationEnabled *bool
|
|
VerificationRequired *bool
|
|
// VerificationContactLinkSet distinguishes "the admin didn't touch this
|
|
// field" (false: keep the current value) from "the admin explicitly
|
|
// chose a link, possibly clearing it to nil" (true: use
|
|
// VerificationContactLinkID as-is, nil included).
|
|
VerificationContactLinkSet bool
|
|
VerificationContactLinkID *uuid.UUID
|
|
}
|
|
|
|
func (s *Service) UpdateCustomerAuth(ctx context.Context, in CustomerAuthInput) (*Settings, error) {
|
|
current, err := s.repo.Get(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
settings := &Settings{
|
|
CustomerLoginEnabled: applyBoolIfSet(in.LoginEnabled, current.CustomerLoginEnabled),
|
|
CustomerRegistrationEnabled: applyBoolIfSet(in.RegistrationEnabled, current.CustomerRegistrationEnabled),
|
|
CustomerVerificationRequired: applyBoolIfSet(in.VerificationRequired, current.CustomerVerificationRequired),
|
|
VerificationContactLinkID: current.VerificationContactLinkID,
|
|
}
|
|
if in.VerificationContactLinkSet {
|
|
settings.VerificationContactLinkID = in.VerificationContactLinkID
|
|
}
|
|
if err := s.repo.UpdateCustomerAuth(ctx, settings); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.Get(ctx)
|
|
}
|
|
|
|
func applyBoolIfSet(value *bool, fallback bool) bool {
|
|
if value == nil {
|
|
return fallback
|
|
}
|
|
return *value
|
|
}
|
|
|
|
// CustomerLoginEnabled is consumed by auth.CustomerHandler to gate the
|
|
// customer login page/endpoint behind the admin's toggle.
|
|
func (s *Service) CustomerLoginEnabled(ctx context.Context) (bool, error) {
|
|
settings, err := s.repo.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return settings.CustomerLoginEnabled, nil
|
|
}
|
|
|
|
// CustomerRegistrationEnabled is consumed by auth.CustomerHandler to gate
|
|
// the customer registration page/endpoint behind the admin's toggle,
|
|
// independently of CustomerLoginEnabled.
|
|
func (s *Service) CustomerRegistrationEnabled(ctx context.Context) (bool, error) {
|
|
settings, err := s.repo.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return settings.CustomerRegistrationEnabled, nil
|
|
}
|
|
|
|
// CustomerAccountsAvailable is consumed by users.Service.Create to prevent
|
|
// creating customer-role users when neither login nor registration is
|
|
// enabled -- such an account could never be used to sign in.
|
|
func (s *Service) CustomerAccountsAvailable(ctx context.Context) (bool, error) {
|
|
settings, err := s.repo.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return settings.CustomerLoginEnabled || settings.CustomerRegistrationEnabled, nil
|
|
}
|
|
|
|
// CustomerCheckoutSettings is consumed by orders.RequireAccountsEnabled /
|
|
// RequireApprovedVerificationIfNeeded to gate order creation.
|
|
func (s *Service) CustomerCheckoutSettings(ctx context.Context) (accountsEnabled, verificationRequired bool, err error) {
|
|
settings, err := s.repo.Get(ctx)
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
return settings.CustomerLoginEnabled, settings.CustomerVerificationRequired, nil
|
|
}
|
|
|
|
// OrdersEnabled is consumed by orders.RequireOrdersEnabled: the site-wide
|
|
// "vitrine vs boutique" switch, checked before any of the customer-account
|
|
// gates so a pure showcase site never has to think about accounts at all.
|
|
func (s *Service) OrdersEnabled(ctx context.Context) (bool, error) {
|
|
settings, err := s.repo.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return settings.OrdersEnabled, nil
|
|
}
|