package handlers import ( "net/http" "strings" "github.com/gin-gonic/gin" "mengyastore-backend/internal/config" "mengyastore-backend/internal/models" "mengyastore-backend/internal/storage" ) type AdminHandler struct { store *storage.JSONStore cfg *config.Config } type productPayload struct { Name string `json:"name"` Price float64 `json:"price"` DiscountPrice float64 `json:"discountPrice"` Tags string `json:"tags"` CoverURL string `json:"coverUrl"` Codes []string `json:"codes"` ScreenshotURLs []string `json:"screenshotUrls"` Description string `json:"description"` Active *bool `json:"active"` } type togglePayload struct { Active bool `json:"active"` } func NewAdminHandler(store *storage.JSONStore, cfg *config.Config) *AdminHandler { return &AdminHandler{store: store, cfg: cfg} } func (h *AdminHandler) GetAdminToken(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"token": h.cfg.AdminToken}) } func (h *AdminHandler) ListAllProducts(c *gin.Context) { if !h.requireAdmin(c) { return } items, err := h.store.ListAll() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": items}) } func (h *AdminHandler) CreateProduct(c *gin.Context) { if !h.requireAdmin(c) { return } var payload productPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) return } screenshotURLs, valid := normalizeScreenshotURLs(payload.ScreenshotURLs) if !valid { c.JSON(http.StatusBadRequest, gin.H{"error": "screenshot urls must be 5 or fewer"}) return } active := true if payload.Active != nil { active = *payload.Active } product := models.Product{ Name: payload.Name, Price: payload.Price, DiscountPrice: payload.DiscountPrice, Tags: normalizeTags(payload.Tags), CoverURL: strings.TrimSpace(payload.CoverURL), Codes: payload.Codes, ScreenshotURLs: screenshotURLs, Description: payload.Description, Active: active, } created, err := h.store.Create(product) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": created}) } func (h *AdminHandler) UpdateProduct(c *gin.Context) { if !h.requireAdmin(c) { return } id := c.Param("id") var payload productPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) return } screenshotURLs, valid := normalizeScreenshotURLs(payload.ScreenshotURLs) if !valid { c.JSON(http.StatusBadRequest, gin.H{"error": "screenshot urls must be 5 or fewer"}) return } active := false if payload.Active != nil { active = *payload.Active } patch := models.Product{ Name: payload.Name, Price: payload.Price, DiscountPrice: payload.DiscountPrice, Tags: normalizeTags(payload.Tags), CoverURL: strings.TrimSpace(payload.CoverURL), Codes: payload.Codes, ScreenshotURLs: screenshotURLs, Description: payload.Description, Active: active, } updated, err := h.store.Update(id, patch) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": updated}) } func (h *AdminHandler) ToggleProduct(c *gin.Context) { if !h.requireAdmin(c) { return } id := c.Param("id") var payload togglePayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) return } updated, err := h.store.Toggle(id, payload.Active) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": updated}) } func (h *AdminHandler) DeleteProduct(c *gin.Context) { if !h.requireAdmin(c) { return } id := c.Param("id") if err := h.store.Delete(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "deleted"}) } func (h *AdminHandler) requireAdmin(c *gin.Context) bool { token := c.Query("token") if token == "" { token = c.GetHeader("Authorization") } if token == h.cfg.AdminToken { return true } c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return false } func normalizeScreenshotURLs(urls []string) ([]string, bool) { cleaned := make([]string, 0, len(urls)) for _, url := range urls { trimmed := strings.TrimSpace(url) if trimmed == "" { continue } cleaned = append(cleaned, trimmed) if len(cleaned) > 5 { return nil, false } } return cleaned, true } func normalizeTags(tagsCSV string) []string { if tagsCSV == "" { return []string{} } parts := strings.Split(tagsCSV, ",") clean := make([]string, 0, len(parts)) seen := map[string]bool{} for _, p := range parts { t := strings.TrimSpace(p) if t == "" { continue } key := strings.ToLower(t) if seen[key] { continue } seen[key] = true clean = append(clean, t) if len(clean) >= 20 { break } } return clean }