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" "net/http" "time" "github.com/go-playground/validator/v10" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" echomiddleware "github.com/labstack/echo/v4/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) } // TODO: templates package was removed during router refactor // This function is unused and should be removed or updated /* func getTemplateUserWithTheme(c echo.Context, queries *database.Queries) (templates.User, error) { // Get the user from context userID := c.Get("user_id") if userID == nil { return templates.User{}, fmt.Errorf("user not authenticated") } // Convert to UUID userUUID, err := uuid.Parse(userID.(string)) if err != nil { return templates.User{}, fmt.Errorf("invalid user ID: %w", err) } // Fetch user from database userDB, err := queries.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) if err != nil { return templates.User{}, err } userTheme := "tokyo-night" if userDB.Theme.Valid { userTheme = userDB.Theme.String } return templates.User{ ID: userID, Email: userEmail, Username: userUsername, Role: userRole, Theme: userTheme, }, nil } */ 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...") // Create login attempt tracker: 5 failed attempts = 15 minute lockout loginAttemptTracker := ratelimit.NewLoginAttemptTracker(5, 15*time.Minute, 5*time.Minute) authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker) systemSettingsHandler := handlers.NewSystemSettingsHandler(queries) libraryHandler := handlers.NewLibraryHandler(queries) deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg) deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries) // Create WebSocket connection manager connManager := sync.NewConnectionManager() // Create sync queue processor queueProcessor := sync.NewSyncQueueProcessor(queries) // Create library service libraryService := services.NewLibraryService(queries) // Create worker for background tasks worker := services.NewWorker(3) koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor) wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware) conflictHandler := handlers.NewConflictHandler(queries, connManager) analyticsHandler := handlers.NewAnalyticsHandler(queries) queueHandler := handlers.NewQueueHandler(queries, queueProcessor) // Create conversion service for EPUB→KEPUB conversion conversionService := services.NewConversionService(queries, "/var/bookhoard/cache/kepub") opdsHandler := handlers.NewOPDSHandler(queries, conversionService) // NEW: Create refactored handlers collectionHandler := handlers.NewCollectionHandler(queries, connManager) dashboardHandler := handlers.NewDashboardHandler(queries) mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker) matchingHandler := handlers.NewMatchingHandler(queries, connManager) 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.Logger()) e.Use(echomiddleware.Recover()) e.Use(echomiddleware.CORS()) 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, DBPool: dbPool, AuthHandler: authHandler, LibraryHandler: libraryHandler, DeviceHandler: deviceHandler, MediaHandler: mediaHandler, MatchingHandler: matchingHandler, KOReaderHandler: koreaderHandler, WSHandler: wsHandler, ConflictHandler: conflictHandler, AnalyticsHandler: analyticsHandler, QueueHandler: queueHandler, CollectionHandler: collectionHandler, DashboardHandler: dashboardHandler, OPDSHandler: opdsHandler, SystemSettingsHandler: systemSettingsHandler, ConnManager: connManager, QueueProcessor: queueProcessor, DeviceAuthMiddleware: deviceAuthMiddleware, LoginTracker: loginAttemptTracker, } // Register all routes and get ebook handler ebookHandler := router.RegisterRoutes(routerConfig) // ======================================================================== // APPLICATION LIFECYCLE MANAGEMENT // ======================================================================== // Create app with lifecycle management application := app.New(e, ebookHandler) // ======================================================================== // START SERVER (managed by app lifecycle) // ======================================================================== log.Printf("Starting server on port %s", cfg.ServerPort) // Start HTTP server in background go func() { if err := e.Start(":" + cfg.ServerPort); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed to start: %v", err) } }() // Start application (blocks until shutdown signal) if err := application.Start(); err != nil { log.Fatalf("Application error: %v", err) } }