Files
template-vitrine/backend/cmd/api/main.go
T
Xor290 919c807004
ci-api / test (push) Failing after 8m7s
ci-web / test (push) Failing after 5m5s
chore: build
2026-09-20 12:18:33 +02:00

204 lines
8.0 KiB
Go

package main
import (
"context"
"log"
"log/slog"
"github.com/gin-gonic/gin"
"backend/internal/modules/auth"
"backend/internal/modules/categories"
"backend/internal/modules/contactlinks"
"backend/internal/modules/customerverification"
"backend/internal/modules/media"
"backend/internal/modules/orders"
"backend/internal/modules/pricing"
"backend/internal/modules/products"
"backend/internal/modules/site"
"backend/internal/modules/telegram"
"backend/internal/modules/units"
"backend/internal/modules/users"
"backend/internal/platform/config"
"backend/internal/platform/db"
"backend/internal/platform/logger"
appmiddleware "backend/internal/platform/middleware"
"backend/internal/platform/redis"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("load config: %v", err)
}
appLogger := logger.New(cfg.Server.GinMode == "debug")
slog.SetDefault(appLogger)
database, err := db.Connect(cfg.Database.URL, cfg.Server.GinMode == "debug")
if err != nil {
log.Fatalf("connect database: %v", err)
}
redisClient, err := redis.Connect(cfg.Redis.URL)
if err != nil {
log.Fatalf("connect redis: %v", err)
}
// --- Site module (wired first: users.Service needs it as its
// customer-accounts gate) ---
siteRepo := site.NewRepository(database)
siteService := site.NewService(siteRepo)
siteHandler := site.NewHandler(siteService)
// --- Users module ---
usersRepo := users.NewRepository(database)
usersService := users.NewService(usersRepo, siteService, cfg.Seed)
usersHandler := users.NewHandler(usersService)
// --- Auth module (admin space mounted; customer space wired but not mounted) ---
refreshStore := auth.NewRefreshStore(redisClient)
authService := auth.NewService(usersService, refreshStore, cfg.JWT.Secret, cfg.JWT.AccessTTL, cfg.JWT.RefreshTTL)
adminAuthHandler := auth.NewAdminHandler(authService, cfg.Server.CookieSecure)
// Customer auth space: registration/login are gated behind
// site.Settings.CustomerAccountsEnabled (see auth.CustomerHandler).
customerAuthHandler := auth.NewCustomerHandler(authService, usersService, siteService, cfg.Server.CookieSecure)
// --- Customer identity verification module (spec: age/ID-restricted
// goods gate). Documents are stored privately, never through media/. ---
verificationStorage, err := customerverification.NewStorage(cfg.Verification.UploadDir)
if err != nil {
log.Fatalf("init verification storage: %v", err)
}
verificationRepo := customerverification.NewRepository(database)
verificationService := customerverification.NewService(verificationRepo, verificationStorage)
verificationHandler := customerverification.NewHandler(verificationService)
// --- Categories module ---
categoriesRepo := categories.NewRepository(database)
categoriesService := categories.NewService(categoriesRepo)
categoriesHandler := categories.NewHandler(categoriesService)
// --- Units module ---
unitsRepo := units.NewRepository(database)
unitsService := units.NewService(unitsRepo)
unitsHandler := units.NewHandler(unitsService)
// --- Products module ---
productsRepo := products.NewRepository(database)
productsService := products.NewService(productsRepo, categoriesRepo)
productsHandler := products.NewHandler(productsService)
// --- Media module (pluggable local/S3 storage). Wired after products:
// each media file is isolated to the single product it was uploaded
// for (or to none, e.g. contact link icons) -- Upload validates
// product_id via productsService, there is no shared media library. ---
mediaStorage, err := buildMediaStorage(cfg.Media)
if err != nil {
log.Fatalf("init media storage: %v", err)
}
mediaRepo := media.NewRepository(database)
mediaService := media.NewService(mediaRepo, mediaStorage, productsService, categoriesRepo, cfg.Media.MaxUploadMB*1024*1024)
mediaHandler := media.NewHandler(mediaService)
// --- Pricing module (quantity-based tiers, spec section 16) ---
pricingRepo := pricing.NewRepository(database)
pricingService := pricing.NewService(pricingRepo)
pricingHandler := pricing.NewHandler(pricingService)
// --- Contact links module (admin-defined list of communication apps
// shown to customers on the storefront: WhatsApp, Telegram, Signal, ...) ---
contactLinksRepo := contactlinks.NewRepository(database)
contactLinksService := contactlinks.NewService(contactLinksRepo)
contactLinksHandler := contactlinks.NewHandler(contactLinksService)
// --- Telegram module (notifications, spec section 20) ---
telegramRepo := telegram.NewRepository(database)
telegramService := telegram.NewService(telegramRepo, telegram.NewHTTPSender(), appLogger)
telegramHandler := telegram.NewHandler(telegramService)
// --- Orders module (depends on products/units/pricing for authoritative
// pricing, and on telegram as its pluggable OrderNotifier) ---
ordersRepo := orders.NewRepository(database)
ordersService := orders.NewService(ordersRepo, productsService, unitsService, pricingService, telegramService)
ordersHandler := orders.NewHandler(ordersService)
requireAdmin := appmiddleware.RequireAdmin(cfg.JWT.Secret)
requireCustomer := appmiddleware.RequireCustomer(cfg.JWT.Secret)
loginRateLimit := appmiddleware.NewPerIPRateLimiter(5, 5).Middleware()
orderRateLimit := appmiddleware.NewPerIPRateLimiter(10, 10).Middleware()
requireOrdersEnabled := orders.RequireOrdersEnabled(siteService)
requireAccountsEnabled := orders.RequireAccountsEnabled(siteService)
requireVerifiedIfNeeded := orders.RequireApprovedVerificationIfNeeded(siteService, verificationService)
gin.SetMode(cfg.Server.GinMode)
router := gin.New()
router.Use(gin.Recovery())
router.Use(gin.Logger())
router.Use(appmiddleware.CORS(cfg.Server.CORSOrigin))
if cfg.Media.Driver == "local" {
router.Static("/uploads", cfg.Media.LocalDir)
}
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
api := router.Group("/api")
auth.RegisterAdminRoutes(api, adminAuthHandler, requireAdmin, loginRateLimit)
auth.RegisterCustomerRoutes(api, customerAuthHandler, requireCustomer, loginRateLimit)
users.RegisterAdminRoutes(api, usersHandler, requireAdmin)
site.RegisterRoutes(api, siteHandler, requireAdmin)
site.RegisterPublicRoutes(api, siteHandler)
media.RegisterAdminRoutes(api, mediaHandler, requireAdmin)
media.RegisterPublicRoutes(api, mediaHandler)
categories.RegisterAdminRoutes(api, categoriesHandler, requireAdmin)
categories.RegisterPublicRoutes(api, categoriesHandler)
contactlinks.RegisterAdminRoutes(api, contactLinksHandler, requireAdmin)
contactlinks.RegisterPublicRoutes(api, contactLinksHandler)
units.RegisterAdminRoutes(api, unitsHandler, requireAdmin)
units.RegisterPublicRoutes(api, unitsHandler)
products.RegisterAdminRoutes(api, productsHandler, requireAdmin)
products.RegisterPublicRoutes(api, productsHandler)
pricing.RegisterAdminRoutes(api, pricingHandler, requireAdmin)
pricing.RegisterPublicRoutes(api, pricingHandler)
telegram.RegisterAdminRoutes(api, telegramHandler, requireAdmin)
customerverification.RegisterCustomerRoutes(api, verificationHandler, requireCustomer)
customerverification.RegisterAdminRoutes(api, verificationHandler, requireAdmin)
orders.RegisterPublicRoutes(api, ordersHandler, orderRateLimit, requireOrdersEnabled, requireAccountsEnabled, requireCustomer, requireVerifiedIfNeeded)
orders.RegisterCustomerRoutes(api, ordersHandler, requireCustomer)
orders.RegisterAdminRoutes(api, ordersHandler, requireAdmin)
appLogger.Info("starting server", "port", cfg.Server.Port)
if err := router.Run(":" + cfg.Server.Port); err != nil {
log.Fatalf("server error: %v", err)
}
}
func buildMediaStorage(cfg config.MediaConfig) (media.Storage, error) {
if cfg.Driver == "s3" {
return media.NewS3Storage(context.Background(), media.S3Config{
Bucket: cfg.S3Bucket,
Region: cfg.S3Region,
Endpoint: cfg.S3Endpoint,
AccessKeyID: cfg.S3AccessKeyID,
SecretKey: cfg.S3SecretKey,
UsePathStyle: cfg.S3UsePathStyle,
PublicBaseURL: cfg.S3PublicBaseURL,
})
}
return media.NewLocalStorage(cfg.LocalDir, cfg.LocalBaseURL)
}