chore: sync
This commit is contained in:
26
mengyastore-backend/internal/config/config.go
Normal file
26
mengyastore-backend/internal/config/config.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AdminToken string `json:"adminToken"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config: %w", err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config: %w", err)
|
||||
}
|
||||
if cfg.AdminToken == "" {
|
||||
cfg.AdminToken = "shumengya520"
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
179
mengyastore-backend/internal/handlers/admin.go
Normal file
179
mengyastore-backend/internal/handlers/admin.go
Normal file
@@ -0,0 +1,179 @@
|
||||
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"`
|
||||
Quantity int `json:"quantity"`
|
||||
CoverURL string `json:"coverUrl"`
|
||||
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 10 or fewer"})
|
||||
return
|
||||
}
|
||||
active := true
|
||||
if payload.Active != nil {
|
||||
active = *payload.Active
|
||||
}
|
||||
product := models.Product{
|
||||
Name: payload.Name,
|
||||
Price: payload.Price,
|
||||
Quantity: payload.Quantity,
|
||||
CoverURL: strings.TrimSpace(payload.CoverURL),
|
||||
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 10 or fewer"})
|
||||
return
|
||||
}
|
||||
active := false
|
||||
if payload.Active != nil {
|
||||
active = *payload.Active
|
||||
}
|
||||
patch := models.Product{
|
||||
Name: payload.Name,
|
||||
Price: payload.Price,
|
||||
Quantity: payload.Quantity,
|
||||
CoverURL: strings.TrimSpace(payload.CoverURL),
|
||||
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) > 10 {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return cleaned, true
|
||||
}
|
||||
26
mengyastore-backend/internal/handlers/public.go
Normal file
26
mengyastore-backend/internal/handlers/public.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"mengyastore-backend/internal/storage"
|
||||
)
|
||||
|
||||
type PublicHandler struct {
|
||||
store *storage.JSONStore
|
||||
}
|
||||
|
||||
func NewPublicHandler(store *storage.JSONStore) *PublicHandler {
|
||||
return &PublicHandler{store: store}
|
||||
}
|
||||
|
||||
func (h *PublicHandler) ListProducts(c *gin.Context) {
|
||||
items, err := h.store.ListActive()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": items})
|
||||
}
|
||||
16
mengyastore-backend/internal/models/product.go
Normal file
16
mengyastore-backend/internal/models/product.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Product struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Quantity int `json:"quantity"`
|
||||
CoverURL string `json:"coverUrl"`
|
||||
ScreenshotURLs []string `json:"screenshotUrls"`
|
||||
Description string `json:"description"`
|
||||
Active bool `json:"active"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
192
mengyastore-backend/internal/storage/jsonstore.go
Normal file
192
mengyastore-backend/internal/storage/jsonstore.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"mengyastore-backend/internal/models"
|
||||
)
|
||||
|
||||
type JSONStore struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewJSONStore(path string) (*JSONStore, error) {
|
||||
if err := ensureFile(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &JSONStore{path: path}, nil
|
||||
}
|
||||
|
||||
func ensureFile(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir data dir: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("stat data file: %w", err)
|
||||
}
|
||||
|
||||
initial := []models.Product{}
|
||||
bytes, err := json.MarshalIndent(initial, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("init json: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, bytes, 0o644); err != nil {
|
||||
return fmt.Errorf("write init json: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JSONStore) ListAll() ([]models.Product, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.readAll()
|
||||
}
|
||||
|
||||
func (s *JSONStore) ListActive() ([]models.Product, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items, err := s.readAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
active := make([]models.Product, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Active {
|
||||
active = append(active, item)
|
||||
}
|
||||
}
|
||||
return active, nil
|
||||
}
|
||||
|
||||
func (s *JSONStore) Create(p models.Product) (models.Product, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items, err := s.readAll()
|
||||
if err != nil {
|
||||
return models.Product{}, err
|
||||
}
|
||||
p = normalizeProduct(p)
|
||||
p.ID = uuid.NewString()
|
||||
now := time.Now()
|
||||
p.CreatedAt = now
|
||||
p.UpdatedAt = now
|
||||
items = append(items, p)
|
||||
if err := s.writeAll(items); err != nil {
|
||||
return models.Product{}, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *JSONStore) Update(id string, patch models.Product) (models.Product, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items, err := s.readAll()
|
||||
if err != nil {
|
||||
return models.Product{}, err
|
||||
}
|
||||
for i, item := range items {
|
||||
if item.ID == id {
|
||||
item.Name = patch.Name
|
||||
item.Price = patch.Price
|
||||
item.Quantity = patch.Quantity
|
||||
item.CoverURL = patch.CoverURL
|
||||
item.ScreenshotURLs = normalizeProduct(patch).ScreenshotURLs
|
||||
item.Description = patch.Description
|
||||
item.Active = patch.Active
|
||||
item.UpdatedAt = time.Now()
|
||||
items[i] = item
|
||||
if err := s.writeAll(items); err != nil {
|
||||
return models.Product{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
return models.Product{}, fmt.Errorf("product not found")
|
||||
}
|
||||
|
||||
func (s *JSONStore) Toggle(id string, active bool) (models.Product, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items, err := s.readAll()
|
||||
if err != nil {
|
||||
return models.Product{}, err
|
||||
}
|
||||
for i, item := range items {
|
||||
if item.ID == id {
|
||||
item.Active = active
|
||||
item.UpdatedAt = time.Now()
|
||||
items[i] = item
|
||||
if err := s.writeAll(items); err != nil {
|
||||
return models.Product{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
return models.Product{}, fmt.Errorf("product not found")
|
||||
}
|
||||
|
||||
func (s *JSONStore) Delete(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items, err := s.readAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filtered := make([]models.Product, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.ID != id {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
if err := s.writeAll(filtered); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *JSONStore) readAll() ([]models.Product, error) {
|
||||
bytes, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read products: %w", err)
|
||||
}
|
||||
var items []models.Product
|
||||
if err := json.Unmarshal(bytes, &items); err != nil {
|
||||
return nil, fmt.Errorf("parse products: %w", err)
|
||||
}
|
||||
for i, item := range items {
|
||||
items[i] = normalizeProduct(item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *JSONStore) writeAll(items []models.Product) error {
|
||||
for i, item := range items {
|
||||
items[i] = normalizeProduct(item)
|
||||
}
|
||||
bytes, err := json.MarshalIndent(items, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode products: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(s.path, bytes, 0o644); err != nil {
|
||||
return fmt.Errorf("write products: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeProduct(item models.Product) models.Product {
|
||||
if item.ScreenshotURLs == nil {
|
||||
item.ScreenshotURLs = []string{}
|
||||
}
|
||||
return item
|
||||
}
|
||||
Reference in New Issue
Block a user