Add DashboardService and DashboardHandler to application configuration: Router Config Updates (internal/router/router.go): - Add services import for DashboardService type - Add DashboardService field to Config struct - DashboardService: Used by SSR routes in frontend.go for data fetching - DashboardHandler: Used by API routes in dashboard.go for JSON endpoints Server Initialization (cmd/server/main.go): - Create dashboardService instance using services.NewDashboardService(queries) - Keep dashboardHandler creation (already exists from Phase 4) - Add DashboardService to routerConfig - Both services now available for dependency injection Test Helpers (cmd/server/tests/test_helpers.go): - Create dashboardService instance for testing - Create dashboardHandler instance for testing - Add both DashboardService and DashboardHandler to routerConfig - Ensures test environment matches production setup Architecture Rationale: - DashboardService: Service layer with business logic (reusable by SSR, mobile) - DashboardHandler: HTTP handler layer (JSON API endpoints) - Separation allows SSR templates to call service directly - API routes use handler for proper HTTP response handling - Mobile apps can use API endpoints via DashboardHandler All three files updated consistently for complete integration.
195 lines
6.1 KiB
Go
195 lines
6.1 KiB
Go
package router
|
|
|
|
import (
|
|
"bookhoard/internal/config"
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/handlers"
|
|
"bookhoard/internal/middleware"
|
|
ratelimit "bookhoard/internal/middleware"
|
|
"bookhoard/internal/services"
|
|
"bookhoard/internal/sync"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
echojwt "github.com/labstack/echo-jwt/v4"
|
|
"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)
|
|
}
|
|
|
|
// Config holds all dependencies needed for route registration
|
|
type Config struct {
|
|
Echo *echo.Echo
|
|
Queries *database.Queries
|
|
Cfg *config.Config
|
|
DBPool interface{} // pgxpool.Pool interface
|
|
AuthHandler *handlers.AuthHandler
|
|
LibraryHandler *handlers.LibraryHandler
|
|
DeviceHandler *handlers.DeviceHandler
|
|
MediaHandler *handlers.MediaHandler
|
|
MatchingHandler *handlers.MatchingHandler
|
|
KOReaderHandler *handlers.KOReaderHandler
|
|
WSHandler *handlers.WSHandler
|
|
ConflictHandler *handlers.ConflictHandler
|
|
AnalyticsHandler *handlers.AnalyticsHandler
|
|
QueueHandler *handlers.QueueHandler
|
|
CollectionHandler *handlers.CollectionHandler
|
|
DashboardHandler *handlers.DashboardHandler
|
|
DashboardService *services.DashboardService
|
|
OPDSHandler *handlers.OPDSHandler
|
|
SystemSettingsHandler *handlers.SystemSettingsHandler
|
|
ConnManager *sync.ConnectionManager
|
|
QueueProcessor *sync.SyncQueueProcessor
|
|
DeviceAuthMiddleware *middleware.DeviceAuthMiddleware
|
|
LoginTracker *ratelimit.LoginAttemptTracker
|
|
ScannerHandler *handlers.Handler
|
|
}
|
|
|
|
// createJWTMiddleware creates a JWT middleware with proper user context setup
|
|
func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc {
|
|
return echojwt.WithConfig(echojwt.Config{
|
|
SigningKey: []byte(cfg.Cfg.JWTSecret),
|
|
ContextKey: "user",
|
|
TokenLookup: "cookie:token,header:Authorization",
|
|
SuccessHandler: func(c echo.Context) {
|
|
token := c.Get("user").(*jwt.Token)
|
|
claims := token.Claims.(jwt.MapClaims)
|
|
c.Set("user_id", claims["user_id"])
|
|
c.Set("user_role", claims["user_role"])
|
|
c.Set("user_email", claims["user_email"])
|
|
c.Set("user_username", claims["user_username"])
|
|
|
|
// Parse UUID from string claims
|
|
userIDStr, _ := claims["user_id"].(string)
|
|
userUUID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user ID in token"})
|
|
return
|
|
}
|
|
|
|
c.Set("user", database.Users{
|
|
ID: pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true},
|
|
Email: claims["user_email"].(string),
|
|
Username: claims["user_username"].(string),
|
|
Role: claims["user_role"].(string),
|
|
})
|
|
},
|
|
ErrorHandler: func(c echo.Context, err error) error {
|
|
// Check if this is a page request (browser navigation)
|
|
if wantsHTML(c.Request().Header) {
|
|
// Page request → Redirect to login with message
|
|
loginURL := "/login?session=expired"
|
|
return c.Redirect(http.StatusFound, loginURL)
|
|
}
|
|
|
|
// API request → Return JSON error
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{
|
|
"error": "session_expired",
|
|
"message": "Your session has expired. Please log in again.",
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
// wantsHTML determines if the request expects HTML response
|
|
func wantsHTML(header http.Header) bool {
|
|
// Check Accept header
|
|
accept := header.Get("Accept")
|
|
if accept != "" && (accept == "text/html" || strings.Contains(accept, "text/html")) {
|
|
return true
|
|
}
|
|
|
|
// Check HTMX request
|
|
if header.Get("HX-Request") == "true" {
|
|
return true
|
|
}
|
|
|
|
// Check for AJAX requests (should get JSON)
|
|
if header.Get("X-Requested-With") == "XMLHttpRequest" {
|
|
return false
|
|
}
|
|
|
|
// Default to JSON for API routes
|
|
return false
|
|
}
|
|
|
|
// RegisterRoutes registers all application routes
|
|
func RegisterRoutes(cfg *Config) *handlers.Handler {
|
|
e := cfg.Echo
|
|
|
|
// Set up validator
|
|
v := validator.New()
|
|
if err := ratelimit.RegisterPasswordValidation(v); err != nil {
|
|
log.Fatal("Failed to register password validator:", err)
|
|
}
|
|
e.Validator = &CustomValidator{validator: v}
|
|
|
|
// Global middleware
|
|
e.Use(echomiddleware.Logger())
|
|
e.Use(echomiddleware.Recover())
|
|
e.Use(echomiddleware.CORS())
|
|
e.Use(ratelimit.RequestTracingMiddleware(cfg.Cfg))
|
|
|
|
// Rate limiter
|
|
rateLimiterConfig := ratelimit.RateLimiterConfig{
|
|
Enabled: cfg.Cfg.RateLimitEnabled,
|
|
RequestsPerMinute: cfg.Cfg.RequestsPerMinute,
|
|
CleanupInterval: 5 * time.Minute,
|
|
}
|
|
rateLimiter := ratelimit.NewRateLimiter(rateLimiterConfig)
|
|
rateLimitMiddleware := ratelimit.RateLimiterMiddleware(rateLimiter)
|
|
|
|
// Register core application routes (collections, devices, media, etc.) - ONCE
|
|
jwtMiddleware := createJWTMiddleware(cfg)
|
|
protected := e.Group("/api", jwtMiddleware)
|
|
|
|
// Create scanner handler for scanner routes and progress routes
|
|
scannerHandler := handlers.SetupRoutes(protected, cfg.Queries, cfg.ConnManager, cfg.QueueProcessor)
|
|
cfg.ScannerHandler = scannerHandler
|
|
|
|
// Register route groups
|
|
registerAuthRoutes(cfg, rateLimitMiddleware)
|
|
registerLibraryRoutes(cfg)
|
|
registerDeviceRoutes(cfg)
|
|
registerSyncRoutes(cfg)
|
|
registerCollectionsRoutes(cfg)
|
|
registerDashboardRoutes(cfg)
|
|
registerMediaRoutes(cfg)
|
|
registerSearchRoutes(cfg)
|
|
registerMatchingRoutes(cfg)
|
|
registerConflictRoutes(cfg)
|
|
registerAnalyticsRoutes(cfg)
|
|
registerQueueRoutes(cfg)
|
|
registerOPDSRoutes(cfg)
|
|
registerWebSocketRoutes(cfg)
|
|
registerFrontendRoutes(cfg)
|
|
registerDocumentationRoutes(cfg)
|
|
e.Static("/static", "web/static")
|
|
|
|
// Start background tasks (queue processor and connection cleanup)
|
|
scannerHandler.StartBackgroundTasks()
|
|
|
|
// Register progress routes with actual handler
|
|
registerProgressRoutes(cfg, scannerHandler)
|
|
|
|
// Register scanner routes (admin only)
|
|
admin := protected.Group("", handlers.AdminMiddleware)
|
|
registerScannerRoutes(admin, scannerHandler)
|
|
|
|
return scannerHandler
|
|
}
|