chore: build
This commit is contained in:
@@ -3,11 +3,40 @@ package site
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var slugPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
|
||||
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
|
||||
@@ -18,13 +47,59 @@ func NewHandler(service *Service) *Handler {
|
||||
}
|
||||
|
||||
type settingsResponse struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
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, Slug: s.Slug}
|
||||
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) {
|
||||
@@ -37,9 +112,9 @@ func (h *Handler) Get(c *gin.Context) {
|
||||
}
|
||||
|
||||
type updateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
OrdersEnabled bool `json:"orders_enabled"`
|
||||
}
|
||||
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
@@ -48,15 +123,155 @@ func (h *Handler) Update(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
if !slugPattern.MatchString(req.Slug) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "slug must be lowercase letters, digits and hyphens only"})
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.service.Update(c.Request.Context(), req.Name, req.Description, req.Slug)
|
||||
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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user