This commit is contained in:
CFOU
2026-09-14 20:50:19 +02:00
commit 3091fb4020
135 changed files with 10262 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
package site
import (
"net/http"
"regexp"
"github.com/gin-gonic/gin"
)
var slugPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
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"`
Slug string `json:"slug"`
}
func toResponse(s *Settings) settingsResponse {
return settingsResponse{Name: s.Name, Description: s.Description, Slug: s.Slug}
}
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"`
Slug string `json:"slug" binding:"required"`
}
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
}
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)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update site settings"})
return
}
c.JSON(http.StatusOK, toResponse(settings))
}
+19
View File
@@ -0,0 +1,19 @@
// Package site owns the minimal, config-driven site identity (name,
// description, slug). It is the proof of the "code defines capabilities,
// admin defines content" pattern: future modules (appearance, menu, pages,
// ...) will follow this same shape (single-row or keyed settings table +
// admin-only read/write endpoints).
package site
import "time"
type Settings struct {
ID int16 `gorm:"primaryKey"`
Name string
Description string
Slug string
CreatedAt time.Time
UpdatedAt time.Time
}
func (Settings) TableName() string { return "site_settings" }
@@ -0,0 +1,37 @@
package site
import (
"context"
"gorm.io/gorm"
)
type Repository interface {
Get(ctx context.Context) (*Settings, error)
Update(ctx context.Context, s *Settings) error
}
type gormRepository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) Repository {
return &gormRepository{db: db}
}
func (r *gormRepository) Get(ctx context.Context) (*Settings, error) {
var s Settings
if err := r.db.WithContext(ctx).First(&s, "id = 1").Error; err != nil {
return nil, err
}
return &s, nil
}
func (r *gormRepository) Update(ctx context.Context, s *Settings) error {
s.ID = 1
return r.db.WithContext(ctx).Model(&Settings{}).Where("id = 1").Updates(map[string]any{
"name": s.Name,
"description": s.Description,
"slug": s.Slug,
}).Error
}
+9
View File
@@ -0,0 +1,9 @@
package site
import "github.com/gin-gonic/gin"
func RegisterRoutes(rg *gin.RouterGroup, h *Handler, requireAdmin gin.HandlerFunc) {
group := rg.Group("/admin/site-settings")
group.GET("", h.Get)
group.PUT("", requireAdmin, h.Update)
}
+23
View File
@@ -0,0 +1,23 @@
package site
import "context"
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, slug string) (*Settings, error) {
settings := &Settings{Name: name, Description: description, Slug: slug}
if err := s.repo.Update(ctx, settings); err != nil {
return nil, err
}
return s.repo.Get(ctx)
}