package main import ( "bookhoard/internal/app" "bookhoard/internal/config" "bookhoard/internal/database" "bookhoard/internal/handlers" "bookhoard/internal/middleware" ratelimit "bookhoard/internal/middleware" "bookhoard/internal/router" "bookhoard/internal/services" "bookhoard/internal/sync" "context" "log" "time" _ "time/tzdata" "github.com/go-playground/validator/v10" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v5" echomiddleware "github.com/labstack/echo/v5/middleware" ) // CustomValidator wraps the go-playground validator type CustomValidator struct { validator *validator.Validate } func (cv *CustomValidator) Validate(i interface{}) error { return cv.validator.Struct(i) } func main() { cfg := config.LoadConfig() dbPool, err := pgxpool.New(context.Background(), cfg.DatabaseURL()) if err != nil { log.Fatal("Failed to connect to database:", err) } defer dbPool.Close() queries := database.New(dbPool) // Initialize database schema log.Println("🔧 Ensuring database schema is initialized...") ctx := context.Background() if err := database.Initialize(ctx, dbPool); err != nil { log.Fatal("❌ Database schema initialization failed:", err) } log.Println("✅ Database schema initialized and verified, starting server...") // Load tunable settings from the DB into the registry. All values fall back // to compiled defaults if a row is missing, so this never blocks startup. registry := database.NewSettingsRegistry(queries) if err := registry.Load(ctx); err != nil { log.Printf("⚠️ Could not load system settings (using defaults): %v", err) } // Wire the registry into the package-level password validator so live // rule changes apply to the echo struct-tag validator and ValidatePassword. middleware.SetDefaultPasswordSettings(registry) // Seed base_url from env var if not already configured. Uses conditional // UPDATE so admin-set values are never overwritten on restart. if cfg.BaseURL != "" { _, err = dbPool.Exec(ctx, ` INSERT INTO system_config (key, value) VALUES ('base_url', $1) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value WHERE system_config.value = '' `, cfg.BaseURL) if err != nil { log.Printf("⚠️ Could not seed base_url: %v", err) } else { // Also seed derived URLs for key, suffix := range map[string]string{ "opds_base_url": "/opds", "api_base_url": "/api", } { _, _ = dbPool.Exec(ctx, ` INSERT INTO system_config (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value WHERE system_config.value = '' `, key, cfg.BaseURL+suffix) } } } // Create login attempt tracker from configured (or default) lockout policy. loginMaxAttempts, loginLockout := registry.LoginLockout() loginAttemptTracker := ratelimit.NewLoginAttemptTracker(loginMaxAttempts, loginLockout, 5*time.Minute) authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker) authHandler.SetSettings(registry) systemSettingsHandler := handlers.NewSystemSettingsHandler(queries) systemSettingsHandler.SetSettings(registry) sidecarHandler := handlers.NewSidecarHandler(queries, cfg) sidecarHandler.SetSettings(registry) libraryHandler := handlers.NewLibraryHandler(queries) deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg) deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries) deviceAuthMiddleware.SetSettings(registry) processingIssuesHandler := handlers.NewProcessingIssuesHandler(queries) // Create WebSocket connection manager connManager := sync.NewConnectionManager() progressService := sync.NewProgressService(queries, connManager) annotationService := sync.NewAnnotationService(queries, connManager) annotationService.SetSettings(registry) tombstonePurgerCancel := annotationService.StartTombstonePurger() defer tombstonePurgerCancel() queueProcessor := sync.NewSyncQueueProcessorWithConfig(queries, registry.SyncQueueConfig().Interval, registry.SyncQueueConfig().BatchSize) queueProcessor.SetProgressService(progressService) queueProcessor.SetAnnotationService(annotationService) // Create library service libraryService := services.NewLibraryService(queries) // Sync Go AllowedExtensions into DB so API clients see correct extensions libraryService.SyncAllowedExtensions(context.Background()) // Create worker for background tasks workerCfg := registry.WorkerPoolConfig() worker := services.NewWorkerWithConfig(workerCfg.Size, workerCfg.QueueCap, connManager) services.WorkerInstance = worker koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor) koreaderHandler.SetProgressService(progressService) koreaderHandler.SetAnnotationService(annotationService) koreaderHandler.SetLibraryService(libraryService) wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware) conflictHandler := handlers.NewConflictHandler(queries, connManager) analyticsHandler := handlers.NewAnalyticsHandler(queries) queueHandler := handlers.NewQueueHandler(queries, queueProcessor) conversionService := services.NewConversionService(queries, "/var/bookhoard/cache/kepub") conversionService.SetSettings(registry) opdsHandler := handlers.NewOPDSHandler(queries, libraryService, conversionService) opdsHandler.SetSettings(registry) collectionHandler := handlers.NewCollectionHandler(queries, libraryService, connManager) dashboardService := services.NewDashboardService(queries) dashboardHandler := handlers.NewDashboardHandler(queries) seriesHandler := handlers.NewSeriesHandler(queries) filtersHandler := handlers.NewFiltersHandler(queries) mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker) mediaHandler.SetProgressService(progressService) mediaHandler.SetAnnotationService(annotationService) matchingHandler := handlers.NewMatchingHandler(queries, connManager) jobsHandler := handlers.NewJobsHandler(queries, worker) e := echo.New() // Set up validator v := validator.New() // Register custom password complexity validator if err := ratelimit.RegisterPasswordValidation(v); err != nil { log.Fatal("Failed to register password validator:", err) } e.Validator = &CustomValidator{validator: v} // Middleware e.Use(echomiddleware.RequestLogger()) e.Use(echomiddleware.Recover()) e.Use(echomiddleware.CORSWithConfig(echomiddleware.CORSConfig{ AllowOrigins: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: false, })) e.Use(ratelimit.RequestTracingMiddleware(cfg)) // Rate limiter for auth endpoints // rateLimiterConfig := ratelimit.RateLimiterConfig{ // Enabled: cfg.RateLimitEnabled, // RequestsPerMinute: cfg.RequestsPerMinute, // CleanupInterval: 5 * time.Minute, // } // rateLimiter := ratelimit.NewRateLimiter(rateLimiterConfig) // rateLimitMiddleware := ratelimit.RateLimiterMiddleware(rateLimiter) // Now in router/auth.go // ======================================================================== // ROUTER REGISTRATION - Migrate routes to internal/router/ package // ======================================================================== routerConfig := &router.Config{ Echo: e, Queries: queries, Cfg: cfg, Settings: registry, DBPool: dbPool, AuthHandler: authHandler, LibraryHandler: libraryHandler, DeviceHandler: deviceHandler, MediaHandler: mediaHandler, MatchingHandler: matchingHandler, ProcessingIssuesHandler: processingIssuesHandler, KOReaderHandler: koreaderHandler, WSHandler: wsHandler, ConflictHandler: conflictHandler, AnalyticsHandler: analyticsHandler, QueueHandler: queueHandler, CollectionHandler: collectionHandler, FiltersHandler: filtersHandler, DashboardHandler: dashboardHandler, DashboardService: dashboardService, SeriesHandler: seriesHandler, OPDSHandler: opdsHandler, Worker: worker, SystemSettingsHandler: systemSettingsHandler, SidecarHandler: sidecarHandler, ConnManager: connManager, QueueProcessor: queueProcessor, ProgressService: progressService, AnnotationService: annotationService, DeviceAuthMiddleware: deviceAuthMiddleware, JobsHandler: jobsHandler, LoginTracker: loginAttemptTracker, LibraryService: libraryService, } // Register all routes and get ebook handler _ = router.RegisterRoutes(routerConfig) // ======================================================================== // APPLICATION LIFECYCLE MANAGEMENT // ======================================================================== // Create app with lifecycle management application := app.New(e) // ======================================================================== // START SERVER (managed by app lifecycle) // ======================================================================== log.Printf("Starting server on port %s", cfg.ServerPort) // Start HTTP server if err := application.StartServer(":" + cfg.ServerPort); err != nil { log.Fatalf("Failed to start server: %v", err) } // Start application lifecycle (blocks until shutdown signal) if err := application.Start(); err != nil { log.Fatalf("Application error: %v", err) } }