- Fix function signatures in reader.go (c echo.Context -> c *echo.Context) - Replace non-existent UserHasLibraryAccess with GetUserVisibleLibraries pattern - Implement SSR reader route in router/reader.go with proper access control - Add inline library access checking following existing codebase patterns - Fix ReadingProgress struct to use LastReadAt instead of CreatedAt/UpdatedAt - Ensure all reader endpoints use consistent library access validation This provides the backend foundation for the reader feature with proper access control and SSR rendering capabilities.
178 lines
5.9 KiB
Go
178 lines
5.9 KiB
Go
package router
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/handlers"
|
|
"bookhoard/internal/services"
|
|
"bookhoard/templates"
|
|
"bytes"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
func registerReaderRoutes(cfg *Config) {
|
|
e := cfg.Echo
|
|
|
|
// Create reader service and handler
|
|
readerService := services.NewReaderService(cfg.Queries, cfg.Worker)
|
|
cfg.ReaderHandler = handlers.NewReaderHandler(
|
|
cfg.Queries,
|
|
services.NewLibraryService(cfg.Queries),
|
|
readerService,
|
|
cfg.Worker,
|
|
)
|
|
|
|
jwtMiddleware := createJWTMiddleware(cfg)
|
|
|
|
// Reader page routes (SSR + API)
|
|
reader := e.Group("/readers", jwtMiddleware)
|
|
|
|
// Reader page (SSR)
|
|
reader.GET("/:mediaItemId", func(c *echo.Context) error {
|
|
// Get user
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
// Parse mediaItemID
|
|
mediaItemID := c.Param("mediaItemId")
|
|
parsedUUID, err := uuid.Parse(mediaItemID)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Invalid media item ID", "invalid_id")
|
|
}
|
|
// Fetch media item (directly, no handler)
|
|
mediaItem, err := cfg.Queries.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
|
|
if err != nil {
|
|
return renderErrorPage(c, "Media item not found", "not_found")
|
|
}
|
|
// Check library access (using pattern from opds.go)
|
|
userUUID, _ := uuid.Parse(user.ID)
|
|
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
|
if err != nil {
|
|
return renderErrorPage(c, "Failed to check access", "access_error")
|
|
}
|
|
|
|
visible := false
|
|
for _, lib := range libraries {
|
|
if lib.ID == mediaItem.LibraryID {
|
|
visible = true
|
|
break
|
|
}
|
|
}
|
|
if !visible {
|
|
return renderErrorPage(c, "Access denied", "access_denied")
|
|
}
|
|
// Get reading progress
|
|
var progress database.ReadingProgress
|
|
progress, err = cfg.Queries.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
|
|
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
|
|
UserID: uuidToPGType(userUUID),
|
|
})
|
|
if err != nil && err != pgx.ErrNoRows {
|
|
progress = database.ReadingProgress{}
|
|
}
|
|
// Get bookmarks
|
|
bookmarks, _ := cfg.Queries.GetMediaBookmarks(c.Request().Context(), database.GetMediaBookmarksParams{
|
|
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
|
|
UserID: uuidToPGType(userUUID),
|
|
})
|
|
// Convert to template types
|
|
mediaUUID, _ := uuid.FromBytes(mediaItem.ID.Bytes[0:16])
|
|
metadata := templates.ReaderMetadata{
|
|
MediaItemID: mediaUUID.String(),
|
|
Title: mediaItem.Title,
|
|
Author: textToString(mediaItem.Author),
|
|
CoverImagePath: textToString(mediaItem.CoverImagePath),
|
|
LibraryType: mediaItem.FormatGroup,
|
|
MimeType: textToString(mediaItem.MimeType),
|
|
FilePath: mediaItem.FilePath,
|
|
TotalPages: int(mediaItem.PageCount.Int32),
|
|
ChapterCount: int(mediaItem.ChapterCount.Int32),
|
|
}
|
|
// Progress conversion (inline)
|
|
progressUUID, _ := uuid.FromBytes(progress.ID.Bytes[0:16])
|
|
progressMediaUUID, _ := uuid.FromBytes(progress.MediaItemID.Bytes[0:16])
|
|
progressUserUUID, _ := uuid.FromBytes(progress.UserID.Bytes[0:16])
|
|
templateProgress := templates.ReadingProgress{
|
|
ID: progressUUID.String(),
|
|
MediaItemID: progressMediaUUID.String(),
|
|
UserID: progressUserUUID.String(),
|
|
CurrentPage: int(progress.CurrentPage.Int32),
|
|
TotalPages: int(progress.TotalPages.Int32),
|
|
Percentage: progress.Percentage.Float64,
|
|
EpubCfi: textToString(progress.Epubcfi),
|
|
LastReadAt: progress.LastReadAt.Time,
|
|
}
|
|
// Bookmarks conversion (inline, with loop)
|
|
templateBookmarks := make([]templates.Bookmark, len(bookmarks))
|
|
for i, b := range bookmarks {
|
|
bookmarkUUID, _ := uuid.FromBytes(b.ID.Bytes[0:16])
|
|
bookmarkMediaUUID, _ := uuid.FromBytes(b.MediaItemID.Bytes[0:16])
|
|
bookmarkUserUUID, _ := uuid.FromBytes(b.UserID.Bytes[0:16])
|
|
|
|
var pageNumber *int
|
|
if b.PageNumber.Valid {
|
|
pn := int(b.PageNumber.Int32)
|
|
pageNumber = &pn
|
|
}
|
|
|
|
var chapterNumber *int
|
|
if b.ChapterNumber.Valid {
|
|
cn := int(b.ChapterNumber.Int32)
|
|
chapterNumber = &cn
|
|
}
|
|
|
|
templateBookmarks[i] = templates.Bookmark{
|
|
ID: bookmarkUUID.String(),
|
|
MediaItemID: bookmarkMediaUUID.String(),
|
|
UserID: bookmarkUserUUID.String(),
|
|
PageNumber: pageNumber,
|
|
ChapterNumber: chapterNumber,
|
|
CfiPosition: textToString(b.CfiPosition),
|
|
Title: b.Title,
|
|
Position: textToString(b.Position),
|
|
Notes: textToString(b.Notes),
|
|
CreatedAt: b.CreatedAt.Time,
|
|
}
|
|
}
|
|
// 8. Render template
|
|
var buf bytes.Buffer
|
|
err = templates.Reader(user, metadata, templateProgress, templateBookmarks).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error rendering reader", "render_error")
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Content serving (lazy-loaded pages)
|
|
reader.GET("/:mediaItemId/pages/:pageNumber", cfg.ReaderHandler.GetPage)
|
|
|
|
// Chapter metadata
|
|
reader.GET("/:mediaItemId/chapters", cfg.ReaderHandler.GetChapters)
|
|
|
|
// Panel data (comics/manga)
|
|
reader.GET("/:mediaItemId/panels/:pageNumber", cfg.ReaderHandler.GetPanels)
|
|
reader.PUT("/:mediaItemId/panels/:pageNumber", cfg.ReaderHandler.UpdatePanels)
|
|
|
|
// PDF outline/TOC
|
|
reader.GET("/:mediaItemId/outline", cfg.ReaderHandler.GetPDFOutline)
|
|
|
|
// PDF thumbnails (for mini-map)
|
|
reader.GET("/:mediaItemId/thumbnails/:pageNumber", cfg.ReaderHandler.GetPDFThumbnail)
|
|
|
|
// Reading speed
|
|
reader.GET("/:mediaItemId/reading-speed", cfg.ReaderHandler.GetReadingSpeed)
|
|
reader.POST("/:mediaItemId/reading-speed", cfg.ReaderHandler.UpdateReadingSpeed)
|
|
|
|
// Dictionary lookup
|
|
reader.GET("/dictionary/:word", cfg.ReaderHandler.LookupWord)
|
|
|
|
// Reader settings
|
|
reader.GET("/settings", cfg.ReaderHandler.GetSettings)
|
|
reader.PUT("/settings", cfg.ReaderHandler.UpdateSettings)
|
|
}
|