package main import ( "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" "bookhoard/templates" "context" "log" "time" "github.com/go-playground/validator/v10" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "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) } func getTemplateUserWithTheme(c echo.Context, queries *database.Queries) (templates.User, error) { userID := c.Get("user_id").(string) userEmail := c.Get("user_email").(string) userUsername := c.Get("user_username").(string) userRole := c.Get("user_role").(string) userUUID, err := uuid.Parse(userID) if err != nil { return templates.User{}, err } 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) // 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) libraryHandler := handlers.NewLibraryHandler(queries) deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg) deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries) // Create WebSocket connection manager connManager := sync.NewConnectionManager() connManager.StartCleanupTask() // Create sync queue processor queueProcessor := sync.NewSyncQueueProcessor(queries) go queueProcessor.Start(context.Background()) 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) 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, KOReaderHandler: koreaderHandler, WSHandler: wsHandler, ConflictHandler: conflictHandler, AnalyticsHandler: analyticsHandler, QueueHandler: queueHandler, CollectionHandler: nil, // TODO: Initialize collection handler OPDSHandler: opdsHandler, ConnManager: connManager, QueueProcessor: queueProcessor, DeviceAuthMiddleware: deviceAuthMiddleware, LoginTracker: loginAttemptTracker, } router.RegisterRoutes(routerConfig) // Public library types endpoint (no authentication required) e.GET("/api/libraries/types", libraryHandler.GetLibraryTypes) // ======================================================================== // FRONTEND ROUTES, HEALTH CHECK, DOCS (all now in router package) // ======================================================================== // Start server log.Printf("Starting server on port %s", cfg.ServerPort) e.Logger.Fatal(e.Start(":" + cfg.ServerPort)) }