278 lines
10 KiB
Go
278 lines
10 KiB
Go
package site
|
|
|
|
import (
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
var (
|
|
colorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
|
|
|
validProductLayouts = map[string]bool{"grid": true, "list": true, "grid-overlay": true, "grid-minimal": true}
|
|
validProductColumns = map[int]bool{0: true, 2: true, 3: true, 4: true, 5: true}
|
|
// validProductScrolls controls whether the storefront product list
|
|
// scrolls the normal way (down the page) or sideways as a swipeable
|
|
// row -- independent from ProductLayout (the card style), so any card
|
|
// style can be browsed either way.
|
|
validProductScrolls = map[string]bool{"vertical": true, "horizontal": true}
|
|
|
|
// validHeroPages are the storefront pages the hero banner can be
|
|
// toggled on for -- kept to a fixed, known set (rather than arbitrary
|
|
// paths) so a stale/invalid value can never sneak into a page that
|
|
// doesn't know what to do with it.
|
|
validHeroPages = map[string]bool{"catalog": true, "home": true, "cart": true, "checkout": true, "account": true, "contact": true}
|
|
)
|
|
|
|
func heroPagesToString(pages []string) string {
|
|
return strings.Join(pages, ",")
|
|
}
|
|
|
|
func heroPagesFromString(s string) []string {
|
|
if s == "" {
|
|
return []string{}
|
|
}
|
|
return strings.Split(s, ",")
|
|
}
|
|
|
|
type Handler struct {
|
|
service *Service
|
|
}
|
|
|
|
func NewHandler(service *Service) *Handler {
|
|
return &Handler{service: service}
|
|
}
|
|
|
|
type settingsResponse struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
OrdersEnabled bool `json:"orders_enabled"`
|
|
|
|
HeaderBgColor string `json:"header_bg_color"`
|
|
HeaderTextColor string `json:"header_text_color"`
|
|
BodyBgColor string `json:"body_bg_color"`
|
|
BodyTextColor string `json:"body_text_color"`
|
|
FooterBgColor string `json:"footer_bg_color"`
|
|
FooterTextColor string `json:"footer_text_color"`
|
|
AccentColor string `json:"accent_color"`
|
|
ProductLayout string `json:"product_layout"`
|
|
ProductColumns int `json:"product_columns"`
|
|
ProductScroll string `json:"product_scroll"`
|
|
|
|
ContactCardTransparent bool `json:"contact_card_transparent"`
|
|
ContactCardBgColor string `json:"contact_card_bg_color"`
|
|
|
|
LogoMediaID *uuid.UUID `json:"logo_media_id"`
|
|
HeroMediaID *uuid.UUID `json:"hero_media_id"`
|
|
HeroPages []string `json:"hero_pages"`
|
|
|
|
CustomerLoginEnabled bool `json:"customer_login_enabled"`
|
|
CustomerRegistrationEnabled bool `json:"customer_registration_enabled"`
|
|
CustomerVerificationRequired bool `json:"customer_verification_required"`
|
|
VerificationContactLinkID *uuid.UUID `json:"verification_contact_link_id"`
|
|
}
|
|
|
|
func toResponse(s *Settings) settingsResponse {
|
|
return settingsResponse{
|
|
Name: s.Name,
|
|
Description: s.Description,
|
|
OrdersEnabled: s.OrdersEnabled,
|
|
HeaderBgColor: s.HeaderBgColor,
|
|
HeaderTextColor: s.HeaderTextColor,
|
|
BodyBgColor: s.BodyBgColor,
|
|
BodyTextColor: s.BodyTextColor,
|
|
FooterBgColor: s.FooterBgColor,
|
|
FooterTextColor: s.FooterTextColor,
|
|
AccentColor: s.AccentColor,
|
|
ProductLayout: s.ProductLayout,
|
|
ProductColumns: s.ProductColumns,
|
|
ProductScroll: s.ProductScroll,
|
|
ContactCardTransparent: s.ContactCardTransparent,
|
|
ContactCardBgColor: s.ContactCardBgColor,
|
|
LogoMediaID: s.LogoMediaID,
|
|
HeroMediaID: s.HeroMediaID,
|
|
HeroPages: heroPagesFromString(s.HeroPages),
|
|
CustomerLoginEnabled: s.CustomerLoginEnabled,
|
|
CustomerRegistrationEnabled: s.CustomerRegistrationEnabled,
|
|
CustomerVerificationRequired: s.CustomerVerificationRequired,
|
|
VerificationContactLinkID: s.VerificationContactLinkID,
|
|
}
|
|
}
|
|
|
|
func (h *Handler) Get(c *gin.Context) {
|
|
settings, err := h.service.Get(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load site settings"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toResponse(settings))
|
|
}
|
|
|
|
type updateRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Description string `json:"description"`
|
|
OrdersEnabled bool `json:"orders_enabled"`
|
|
}
|
|
|
|
func (h *Handler) Update(c *gin.Context) {
|
|
var req updateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
settings, err := h.service.Update(c.Request.Context(), req.Name, req.Description, req.OrdersEnabled)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update site settings"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toResponse(settings))
|
|
}
|
|
|
|
// updateAppearanceRequest fields are all optional pointers: the admin can
|
|
// send just the one option they changed (e.g. {"product_layout": "list"})
|
|
// and every other appearance setting keeps its current value.
|
|
type updateAppearanceRequest struct {
|
|
HeaderBgColor *string `json:"header_bg_color"`
|
|
HeaderTextColor *string `json:"header_text_color"`
|
|
BodyBgColor *string `json:"body_bg_color"`
|
|
BodyTextColor *string `json:"body_text_color"`
|
|
FooterBgColor *string `json:"footer_bg_color"`
|
|
FooterTextColor *string `json:"footer_text_color"`
|
|
AccentColor *string `json:"accent_color"`
|
|
ProductLayout *string `json:"product_layout"`
|
|
ProductColumns *int `json:"product_columns"`
|
|
ProductScroll *string `json:"product_scroll"`
|
|
|
|
ContactCardTransparent *bool `json:"contact_card_transparent"`
|
|
ContactCardBgColor *string `json:"contact_card_bg_color"`
|
|
|
|
LogoMediaID *uuid.UUID `json:"logo_media_id"`
|
|
ClearLogo bool `json:"clear_logo"`
|
|
HeroMediaID *uuid.UUID `json:"hero_media_id"`
|
|
ClearHero bool `json:"clear_hero"`
|
|
HeroPages *[]string `json:"hero_pages"`
|
|
}
|
|
|
|
func (h *Handler) UpdateAppearance(c *gin.Context) {
|
|
var req updateAppearanceRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()})
|
|
return
|
|
}
|
|
for _, color := range []*string{
|
|
req.HeaderBgColor, req.HeaderTextColor,
|
|
req.BodyBgColor, req.BodyTextColor,
|
|
req.FooterBgColor, req.FooterTextColor,
|
|
req.AccentColor, req.ContactCardBgColor,
|
|
} {
|
|
if color != nil && !colorPattern.MatchString(*color) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "colors must be hex values like #RRGGBB"})
|
|
return
|
|
}
|
|
}
|
|
if req.ProductLayout != nil && !validProductLayouts[*req.ProductLayout] {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "product_layout must be one of: grid, list, grid-overlay, grid-minimal"})
|
|
return
|
|
}
|
|
if req.ProductColumns != nil && !validProductColumns[*req.ProductColumns] {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "product_columns must be one of: 0 (auto), 2, 3, 4, 5"})
|
|
return
|
|
}
|
|
if req.ProductScroll != nil && !validProductScrolls[*req.ProductScroll] {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "product_scroll must be one of: vertical, horizontal"})
|
|
return
|
|
}
|
|
var heroPagesStr *string
|
|
if req.HeroPages != nil {
|
|
for _, page := range *req.HeroPages {
|
|
if !validHeroPages[page] {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "hero_pages must only contain: home, cart, checkout, account, contact"})
|
|
return
|
|
}
|
|
}
|
|
joined := heroPagesToString(*req.HeroPages)
|
|
heroPagesStr = &joined
|
|
}
|
|
|
|
in := AppearanceInput{
|
|
HeaderBgColor: req.HeaderBgColor,
|
|
HeaderTextColor: req.HeaderTextColor,
|
|
BodyBgColor: req.BodyBgColor,
|
|
BodyTextColor: req.BodyTextColor,
|
|
FooterBgColor: req.FooterBgColor,
|
|
FooterTextColor: req.FooterTextColor,
|
|
AccentColor: req.AccentColor,
|
|
ProductLayout: req.ProductLayout,
|
|
ProductColumns: req.ProductColumns,
|
|
ContactCardTransparent: req.ContactCardTransparent,
|
|
ContactCardBgColor: req.ContactCardBgColor,
|
|
ProductScroll: req.ProductScroll,
|
|
HeroPages: heroPagesStr,
|
|
}
|
|
if req.ClearLogo {
|
|
in.LogoMediaIDSet = true
|
|
in.LogoMediaID = nil
|
|
} else if req.LogoMediaID != nil {
|
|
in.LogoMediaIDSet = true
|
|
in.LogoMediaID = req.LogoMediaID
|
|
}
|
|
if req.ClearHero {
|
|
in.HeroMediaIDSet = true
|
|
in.HeroMediaID = nil
|
|
} else if req.HeroMediaID != nil {
|
|
in.HeroMediaIDSet = true
|
|
in.HeroMediaID = req.HeroMediaID
|
|
}
|
|
|
|
settings, err := h.service.UpdateAppearance(c.Request.Context(), in)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update appearance settings"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toResponse(settings))
|
|
}
|
|
|
|
// updateCustomerAuthRequest fields are all optional: the admin can flip
|
|
// just one toggle at a time. ClearVerificationContactLink is a separate
|
|
// explicit flag (rather than overloading a nil ID) so "field omitted" and
|
|
// "admin picked no link" are unambiguous.
|
|
type updateCustomerAuthRequest struct {
|
|
LoginEnabled *bool `json:"login_enabled"`
|
|
RegistrationEnabled *bool `json:"registration_enabled"`
|
|
VerificationRequired *bool `json:"verification_required"`
|
|
VerificationContactLinkID *uuid.UUID `json:"verification_contact_link_id"`
|
|
ClearVerificationContactLink bool `json:"clear_verification_contact_link"`
|
|
}
|
|
|
|
func (h *Handler) UpdateCustomerAuth(c *gin.Context) {
|
|
var req updateCustomerAuthRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
in := CustomerAuthInput{
|
|
LoginEnabled: req.LoginEnabled,
|
|
RegistrationEnabled: req.RegistrationEnabled,
|
|
VerificationRequired: req.VerificationRequired,
|
|
}
|
|
if req.ClearVerificationContactLink {
|
|
in.VerificationContactLinkSet = true
|
|
in.VerificationContactLinkID = nil
|
|
} else if req.VerificationContactLinkID != nil {
|
|
in.VerificationContactLinkSet = true
|
|
in.VerificationContactLinkID = req.VerificationContactLinkID
|
|
}
|
|
|
|
settings, err := h.service.UpdateCustomerAuth(c.Request.Context(), in)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update customer auth settings"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toResponse(settings))
|
|
}
|