All four progress write paths now delegate to ProgressService.SaveProgress: - MediaHandler: UpdateMediaReadingProgress uses ProgressService for web saves with richer request body (reading_mode, zoom_level, scroll). GET now uses GetUniversalProgress query that JOINs media_items for format_group, total_characters, chapter_count. - KOReaderHandler: updateProgressForBook delegates to ProgressService. Fixed device ID bug (was using userID, now uses deviceID). Removed duplicate UpdateDeviceLastSync with zero UUID. Added pgtype helper functions (textPtrToPgText, intPtrToPgInt4, int64PtrToPgInt8). - KoboHandler: all four progress write points (Markup ReadingSync, Markup last-read-place, AnalyticsGettests, SyncFromServer) delegate to ProgressService. Fixed empty epubcfi string now correctly set to Valid: false. SyncFromServer preserves last_sync_source=bookhoard and Broadcast: false. - QueueProcessor: syncProgress delegates to ProgressService. - main.go: creates ProgressService after ConnectionManager, injects via SetProgressService() on all handlers and queue processor. Handler tests cover pgtype conversion helpers (textPtrToPgText, etc.) and device icon mapping.
189 lines
6.9 KiB
Go
189 lines
6.9 KiB
Go
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"
|
|
|
|
"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...")
|
|
|
|
// 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)
|
|
sidecarHandler := handlers.NewSidecarHandler(queries, cfg)
|
|
libraryHandler := handlers.NewLibraryHandler(queries)
|
|
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
|
|
deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries)
|
|
processingIssuesHandler := handlers.NewProcessingIssuesHandler(queries)
|
|
|
|
// Create WebSocket connection manager
|
|
connManager := sync.NewConnectionManager()
|
|
|
|
progressService := sync.NewProgressService(queries, connManager)
|
|
|
|
queueProcessor := sync.NewSyncQueueProcessor(queries)
|
|
queueProcessor.SetProgressService(progressService)
|
|
|
|
// Create library service
|
|
libraryService := services.NewLibraryService(queries)
|
|
|
|
// Create worker for background tasks
|
|
worker := services.NewWorker(3, connManager)
|
|
services.WorkerInstance = worker
|
|
|
|
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
|
koreaderHandler.SetProgressService(progressService)
|
|
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")
|
|
opdsHandler := handlers.NewOPDSHandler(queries, libraryService, conversionService)
|
|
|
|
collectionHandler := handlers.NewCollectionHandler(queries, libraryService, connManager)
|
|
dashboardService := services.NewDashboardService(queries)
|
|
dashboardHandler := handlers.NewDashboardHandler(queries)
|
|
filtersHandler := handlers.NewFiltersHandler(queries)
|
|
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
|
|
mediaHandler.SetProgressService(progressService)
|
|
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,
|
|
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,
|
|
OPDSHandler: opdsHandler,
|
|
Worker: worker,
|
|
SystemSettingsHandler: systemSettingsHandler,
|
|
SidecarHandler: sidecarHandler,
|
|
ConnManager: connManager,
|
|
QueueProcessor: queueProcessor,
|
|
ProgressService: progressService,
|
|
DeviceAuthMiddleware: deviceAuthMiddleware,
|
|
JobsHandler: jobsHandler,
|
|
LoginTracker: loginAttemptTracker,
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|