Wire up the ProcessingIssuesHandler throughout the application: cmd/server/main.go: - Remove obsolete commented-out getTemplateUserWithTheme function - Instantiate ProcessingIssuesHandler with database queries - Add handler to router Config (with field alignment cleanup) internal/router/router.go: - Add ProcessingIssuesHandler field to router Config struct - Reformat Config struct for better field alignment This enables the processing issues API endpoints for listing and getting statistics about issues within libraries, integrated with the admin UI.
276 lines
8.5 KiB
Go
276 lines
8.5 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"
|
|
"bookhoard/templates"
|
|
"bytes"
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"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/v5"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
// 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
|
|
ProcessingIssuesHandler *handlers.ProcessingIssuesHandler
|
|
KOReaderHandler *handlers.KOReaderHandler
|
|
WSHandler *handlers.WSHandler
|
|
ConflictHandler *handlers.ConflictHandler
|
|
AnalyticsHandler *handlers.AnalyticsHandler
|
|
QueueHandler *handlers.QueueHandler
|
|
CollectionHandler *handlers.CollectionHandler
|
|
Worker *services.Worker
|
|
FiltersHandler *handlers.FiltersHandler
|
|
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
|
|
JobsHandler *handlers.JobsHandler
|
|
SidecarHandler *handlers.SidecarHandler
|
|
ReaderHandler *handlers.ReaderHandler
|
|
}
|
|
|
|
// 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:Bearer ",
|
|
SuccessHandler: func(c *echo.Context) error {
|
|
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 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "invalid user ID in token")
|
|
}
|
|
|
|
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),
|
|
})
|
|
return nil
|
|
},
|
|
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.",
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
// ensureUserExistsMiddleware checks if the authenticated user still exists in the database
|
|
func ensureUserExistsMiddleware(cfg *Config) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c *echo.Context) error {
|
|
userIDStr, ok := c.Get("user_id").(string)
|
|
if !ok {
|
|
return next(c)
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
log.Printf("Invalid UUID in user existence check: %v", err)
|
|
return next(c)
|
|
}
|
|
|
|
// Check if user exists in database
|
|
_, err = cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(userUUID))
|
|
if err != nil {
|
|
log.Printf("User not found in database: %s", userIDStr)
|
|
|
|
// Clear invalid cookie
|
|
c.SetCookie(&http.Cookie{
|
|
Name: "token",
|
|
Value: "",
|
|
Expires: time.Now().Add(-24 * time.Hour),
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
|
|
// Redirect to login with session=invalid
|
|
return c.Redirect(http.StatusFound, "/login?session=invalid")
|
|
}
|
|
|
|
return next(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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}
|
|
|
|
// 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.Cfg)
|
|
cfg.ScannerHandler = scannerHandler
|
|
|
|
// Register route groups
|
|
registerAuthRoutes(cfg, rateLimitMiddleware)
|
|
registerLibraryRoutes(cfg)
|
|
registerDeviceRoutes(cfg)
|
|
registerSystemRoutes(cfg)
|
|
registerSyncRoutes(cfg)
|
|
registerCollectionsRoutes(cfg)
|
|
registerDashboardRoutes(cfg)
|
|
registerMediaRoutes(cfg)
|
|
registerSearchRoutes(cfg)
|
|
registerMatchingRoutes(cfg)
|
|
registerConflictRoutes(cfg)
|
|
registerAnalyticsRoutes(cfg)
|
|
registerQueueRoutes(cfg)
|
|
registerJobRoutes(cfg)
|
|
registerFiltersRoutes(cfg)
|
|
registerOPDSRoutes(cfg)
|
|
registerReaderRoutes(cfg)
|
|
registerWebSocketRoutes(cfg)
|
|
registerFrontendRoutes(cfg)
|
|
registerDocumentationRoutes(cfg)
|
|
e.Static("/static", "web/static")
|
|
|
|
// Catch-all 404 handler - must be last
|
|
e.GET("/*", func(c *echo.Context) error {
|
|
var buf bytes.Buffer
|
|
err := templates.ErrorPage("Page not found", "404").Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusNotFound, buf.String())
|
|
})
|
|
|
|
// Start background tasks (queue processor and connection cleanup)
|
|
scannerHandler.StartBackgroundTasks()
|
|
|
|
// Start watch mode for all libraries (after 2 second delay for DB)
|
|
go func() {
|
|
time.Sleep(2 * time.Second)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
setting, err := cfg.Queries.GetSystemSetting(ctx, "auto_scan_enabled")
|
|
enabled := true // default
|
|
if err == nil && setting != "" {
|
|
enabled, _ = strconv.ParseBool(setting)
|
|
}
|
|
|
|
if !enabled {
|
|
log.Println("Auto-scan disabled in settings, skipping watch mode startup")
|
|
return
|
|
}
|
|
|
|
log.Println("Starting watch mode for all libraries...")
|
|
if err := scannerHandler.StartWatchModeForAllLibraries(context.Background()); err != nil {
|
|
log.Printf("Failed to start watch mode: %v", err)
|
|
} else {
|
|
log.Println("Watch mode started successfully")
|
|
}
|
|
}()
|
|
|
|
// Register progress routes with actual handler
|
|
registerProgressRoutes(cfg, scannerHandler)
|
|
|
|
// Register scanner routes (admin only)
|
|
admin := protected.Group("", handlers.AdminMiddleware)
|
|
registerScannerRoutes(admin, scannerHandler)
|
|
|
|
return scannerHandler
|
|
}
|