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) }